I do not see ~default
argument being evaluated lazily.
let value o ~default = match o with Some v -> v | None -> default
I can do it with Option.fold
but it seems overkill
I do not see ~default
argument being evaluated lazily.
let value o ~default = match o with Some v -> v | None -> default
I can do it with Option.fold
but it seems overkill
Thanks @Chet_Murthy for suggesting the Monad Way. I’ve tried the let+
and it works but I will not call it an improvement.
Judge for yourself
let ( let// ) v f = match v with Some _ -> v | None -> f ();;
let// x = Some 1 in Some 3 (* x is Some 1 *);;
let// x = None in Some 3 (* x is Some 3 *);;
let f () = print_endline "********"; Some 9;;
let// x = Some 1 in f () (* x is Some 1 *);;
let// x = None in f () (* x is Some 9 and some stars are printed *);;
I’ll probably stick with pattern matching for now.
But now I’d like to have something like that in Stdlib
module Option
...
let bind o f = match o with None -> None | Some v -> f v
..
module Syntax = struct
let ( let+ ) = bind
end
end
There is an open PR to add let-operators to the OCaml Stdlib for the various monadic types.
Oh, now here’s another way to do this, that … seems syntactically more … “pleasant”?
module OM = struct let (>>=) v f = match v with Some _ -> v | None -> Lazy.force f end ;;
So you use it thus:
# OM.( (Some 1) >>= lazy (Some 2)) ;;
- : int option = Some 1
# OM.( None >>= lazy (Some 2)) ;;
- : int option = Some 2
Not arguin’ for it – just noting that it seems … pleasant.
I wonder whether it’s efficient compared to the method with closures.
Someone on here some time ago came up with a let|| (let-or) operator which I noted down at the time because it seemed quite interesting. It will accept the first option which does not evaluate to None. You can make the default the last value:
let (let||) opt f =
match opt with
| Some _ -> opt
| None -> f ()
So you could have this:
# let x =
let|| () = Some 10 in
let|| () = Some 20 in
Some 30;;
- : val x : int option = Some 10
let x =
let|| () = None in
let|| () = None in
Some 30;;
- : val x : int option = Some 30
The fact that the (let||) operator automatically puts everything after the first binding into a thunk, and so on thereafter, is the nice part I think, because it gives you proper short-circuiting. I think that outweighs the slight complication of the syntax.
I’m feeling this sort of … disquiet, that there’s no way to get this same functionality using infix operators. Let is fine for sure, but it seems unfortunate that there isn’t some way to have an infix operator that implicitly lazy-i-fies its right-hand-side argument, so that we can express short-circuit-ing without using let.
Well that’s been a long-time OCaml philosophy, right? To be very explicit about everything. It’s only recently that we’re seeing some implicit behaviours start to come in to the language.
Thanks to everyone who replied. Just for fun here is one more way, which is probably overkill
let orelse x y = match x with Some _ -> x | None -> Lazy.force y;;
let ( /* ) x f = f x and ( */ ) f x = f x;;
let _ = Some 1 /*orelse*/ lazy (Some 2);;
let _ = None /*orelse*/ lazy (Some 2);;
I must confess that the /*
something */
trick confused when I first saw it.
Ha, I hope no one is actually doing this
One says to oneself idly “gee, I see an International Obfuscated Ocaml Contest” in our future!
Most people would call that an ‘OCaml Contest’
I wrote two of these functions in my app; <||>
is my lazy version, e.g. Some(5) <||> lazy (Some (6+3))
, and <|>
for cases that can use a strict second argument. I decided I didn’t want to overload boolean ||
.
I also added <??>
for the “or default” variant of this logic, e.g. Some(5) <??> lazy (6+3)
), and <?!>
which takes a strict value. I chose this name because I’m teaching ReasonML to JS programmers, and it’s modelled on the JavaScript nullish coalescing operator (??
). Unfortunately, while I can define ??
, the associativity difference renders it fairly pointless in use.
One thing to keep in mind is that <||>
is left-associative, so if you have a long chain of them, a lazy thunk will be created for each before evaluating the option-expressions left-to-right. That has the intended semantics, but might allocate more than you expect. The let||
version, on the other hand, should create one closure for each option-expression that evaluates to None
. Presuming that the compiler doesn’t inline / hoist / etc. those closures away, which would be even better.
Ah, yeah, good point. So far we’ve only used it for small expressions, not long chains, but I will note that in the docs. Thanks!
I actually am a fan of monads, which is maybe why this approach bothers me?
I don’t deny that the short-circuit thing is useful, but using the bind operator (or even worse, overriding let||
) is problematic, since this usecase has nothing to do with the notion of binding.
The implementation is clever, but using >>=
or let||
in this way is somehow akin to
module Wat = struct
let ( + ) a b = a * b
end
i.e. these symbols have established meanings, and this usecase breaks those expectations. If such a behavior were added to the standard library (or any other library), I think it would make a lot more sense to use another operator.
this discussion reminds me of something I did recently. When I experiment a program, I like to print lots of debug information. Because I’m lazy I would usually do something like this:
let pr s = if !debug then print_endline s else ()
and then print debug info like this:
sprintf "Here is the result = %f." x |> pr
Of course I know all these sprintf are evaluated even if !debug is false. So finally I decided that the computer should be Lazy, not I, and I tried:
let lazy_pr s = if !debug then print_endline (Lazy.force s) else ()
...
lazy (sprintf "Here is the result = %f." x) |> lazy_pr
Result: of course the syntax is less appealing, but since I was using many of them in large loops, the lazy version increased the speed of my program by about 4% or 5%.
(with !debug=false
, of course)
This makes me think…
Let* seems to have become the commonly accepted alias for bind, and let+ for map. let|| seems to be entirely up for grabs. (I agree that having >>= as other than the bind operator is poor practice.)