Hey, in python I would often have the following pattern:
def f(x, y, z):
if x == 1:
return 0
if y == 2:
return 0
return z
How can I achieve the same compact representation in ocaml? This is not about the identical pieces of code. This is about the general approach. This doesn’t look pretty:
let f x y z =
(
let Option.Let_syntax in
let%bind () = Option.some_if (x != 1) () in
let%map () = Option.some_if (y != 2) () in
z)
|> Option.value ~default:0
I don’t quite follow that little snippet, but maybe you mean something like “what if in the original example, x,y were options ? In such situations (and they -have- come up in code I’ve written, and with enough frequency that this is my standard pattern) I would do:
let f (x : int option) (y : int option) z =
if x = Some 1 then
0
else if y = Some 2 then
0
else z
I don’t know if this is a helpful answer, but coming from Python I had never worked with match statements before. I am using them where I would normally have done an if/elif thing, and I am beginning to appreciate that I think they could be pretty powerful.
Ah yes, now I see it. Thank you. I was missing that what the OP was asking for is not so much “how to do if..elif…elif …” but how to do “return”. And your pattern is how to do it (either that, or use monads).
Python is a procedural language, not a functional one, even as it has a functional part. OCaml is a functional language, not a procedural language. So “return” simply doesn’t -exist-, it would have no -meaning-. Every expression “returns” its value. A function is a list of formals, and then an expression; the return-value of a function application is value to which that expression evaluates.
I often write a single match over multiple values, ordering left-to-right from the first that’s checked to the last:
let f x y z =
match x, y with
| 1, _
| _, 2 -> 0
| _, _ -> z
it extends quite well to when you have some conditions that depend on several of the arguments
it also allows you to be more explicit (e.g., if you’ve tested a var for true then you can add false to other patterns)
All true, but I think this makes the style of coding in the OP’s message sound a little more alien than it really is. OCaml has a Result monad, and it has do notation, and the “Early Return monad” is just a special case of Result:
module Early : sig
type ('a, 'r) m
type 'r t = ('r, 'r) m
val ( let* ) : ('a, 'r) m -> ('a -> ('b, 'r) m) -> ('b, 'r) m
val return : 'a -> ('a, 'r) m
val early : 'r -> ('a, 'r) m
val run : 'r t -> 'r
end = struct
type ('a, 'r) m = ('a, 'r) Result.t
type 'r t = ('r, 'r) m
let ( let* ) = Result.bind
let return = Result.ok
let early = Result.error
let run = function Ok v -> v | Error v -> v
end
let compute : (int -> int) -> int -> int -> int =
fun expensive_calc a b ->
let open Early in
run
begin
let* () = if a + b = 0 then early 0 else return () in
let factor_1 = expensive_calc a in
let* factor_2 =
if factor_1 = 0 then early 0 else return @@ expensive_calc b
in
return ((a + b) * factor_1 * factor_2)
end
But as others have eluded too, if you feel yourself reaching for return often it may be a sign that you haven’t internalized the idomatic way of doing things in OCaml yet. Idiomatic code rarely needs “early return” via exception; you can generally just use chained conditionals or matches as others have suggested.
I had made a note somewhere that when using this pattern (very local use of exception without a risk of the exception coming from somewhere else or escape) you’d probably want to use raise_notrace instead of raise here. Does this sound right?
By the way, Base.Exn.raise_without_backtrace clears the backtrace before running raise_notrace, and this is what is in fine used by the Base.With_return module also mentioned in this discussion. I don’t understand the detail on that last point, I’m just pointing it out as a source of divergence compared to that pattern. Please let me know if you know!
The loop and local exception is, imho, perfectly idiomatic. A recent
example for me is this helper, which I prefer to a tailrec function:
(** Update loop for atomics *)
let update_cas (type res) (self : 'a Atomic.t) (f : 'a -> res * 'a) : res =
let exception Ret of res in
let backoff = ref 1 in
try
while true do
let old_val = Atomic.get self in
let res, new_val = f old_val in
if Atomic.compare_and_set self old_val new_val then
raise_notrace (Ret res);
for _i = 1 to !backoff do
Domain.cpu_relax ()
done;
backoff := min 128 (2 * !backoff)
done
with Ret r -> r
dune exec -- ./exn_cost.exe -ascii -quota 1 -clear-columns time cycles
Estimated testing time 4s (4 benchmarks x 1s). Change using '-quota'.
Name Time/Run Cycls/Run
----------------------- ---------- -----------
simple computation 1.84ns 3.66c
computation w/handler 3.13ns 6.23c
end with exn 27.96ns 55.69c
end with exn notrace 11.69ns 23.28c
(It would be nice if the Real World example was redone using local exceptions.)
So yes @c-cube@Chet_Murthy@mbarbin: it can be idiomatic especially for short-circuiting loops, and monads and tailrec are the most conventional, and local exceptions should be used with raise_notrace (TIL).
I just want to come back to this little snippet. It raises two thoughts:
(1) This -is- the way to do it in OCaml. Each language has idioms that are …. “intended”, and others that are “useful” or “it works”. And exceptions are -the- “intended” way to do nonlocal but AST-structural control-flow.
Of course, the semanticist knows they can be converted (via a monad) into pure functional code, but OCaml was never supposed to be a pure functional language
(2) It’s a little bit of a pity there isn’t just a little syntactic support for this to make it more …. seamless. In Rust, there is such syntactic support (IIRC).
In a spicier version, for CAS loops that only return unit, I’ve used this a bit. It’s less idiomatic but I also find it kind of funny:
module Foo = struct
type t = { spans: Span.t list Atomic.t } [@@unboxed]
let create () : t = { spans = Atomic.make [] }
let get self = Atomic.get self.spans
let add (self : t) span =
while
let old = Atomic.get self.spans in
not (Atomic.compare_and_set self.spans old (span :: old))
do
some_sort_of_backoff ()
done
end
just do everything in the while’s condition and use false to exit!
But more seriously yes I’m very jealous of rust’s
let x = loop {
do_stuff;
if whatever { break 42 }
};
There is a trade-off between raise_notrace and Base.Exn.raise_without_backtrace. The benefit of raise_notrace is that you can use it in code that might run in an exception handler without messing up the backtrace of the exception being handled; say, when constructing some diagnostics prior to reraising the exception. The benefit of Base.Exn.raise_without_backtrace is that if you mistakenly let the raised exception propagate out, it will not be mis-associated with the backtrace of whatever exception was last raised, which has the potential to cause great confusion. On the other hand, if used in an exception handler, it will destroy the backtrace of the in-flight exception.
let with_return (type a) (f : return:(a -> _) -> a) =
let exception Ret of a in
let return x = raise_notrace (Ret x) in
try f ~return with Ret x -> x
to abstract the pattern, making scoped early returns a bit more concise
# let f () =
with_return @@ fun ~return ->
while true do
Printf.printf "> ";
let i = read_int () in
if i > 10 then return i;
Printf.printf "%d\n" i
done;;
val f : unit -> int = <fun>
# f ();;
> 11
- : int = 11
# f ();;
> 2
2
> 1
1