Difference between let+, let* and let()?
Asked Answered
M

2

5

As the documentation on OCaml is sparse, i would appreciate if some one can explain the difference in different flavors of let usage.

I tried looking into https://dev.realworldocaml.org/toc.html, but there is no easy way to search in the website. Google search landed me to some articles, but did not get the exact explanation.

Michelle answered 8/1, 2023 at 23:38 Comment(1)
let* and let+ are syntactic sugar for Monads and Applicatives. Similar to Haskells do notation.Spinnaker
A
5

The basic form of let expressions is:

let p1 = e1
and p2 = e2
...
and pN = eN
in e

where N is at least 1. In this form, let expressions pattern matches the value that results from evaluating the RHS expressions against the LHS patterns, then evaluates the body with the new bindings defined by the LHS patterns in scope. For example,

let x, y = 1, 2 in
x + y

evaluates to 3.

When let has an operator name attached, it is the application of what is called a "let operator" or "binding operator" (to give you easier terms to search up). For example:

let+ x, y = 1, 2 in
x + y

desugars to (let+) (1, 2) (fun (x, y) -> x + y). (Similar to how one surrounds the operator + in parentheses, making it (+), to refer to its identifier, the identifier for the let operator let+, as it appears in a let expression, would be (let+).)

Finally, when a let binding has an operator name attached, all the and bindings must have operator names attached as well.

let* x = 1
and+ y = 2
and* z = 3 in
x + y + z

desugars to (let*) ((and+) 1 ((and*) 2 3)) (fun ((x, y), z) ->).

The following program is invalid and has no meaning because the let binding is being used as an operator, but the and binding is not:

let* x = 1
and y = 2 in
x + y

Binding operators are covered in the "language extensions" section of the OCaml documentation.

let () = e is merely the non-operator form of a pattern match, where () is the pattern that matches the only value of the unit type. The unit type is conventionally the type of expressions that don't evaluate to a meaningful value, but exist for side effects (e.g. print_endline "Hello world!"). Matching against () ensures that the expression has type (), catching partial application errors. The following typechecks:

let f x y =
  print_endline x;
  print_endline y

let () =
  f "Hello" "World"

The following does not:

let f x y =
  print_endline x;
  print_endline y

let () =
  f "Hello" (* Missing second argument, so expression has type string -> unit, not unit *)

Note that the binding operators are useful for conveniently using "monads" and "applicatives," so you may hear these words when learning about binding operators. However, binding operators are not inherently related to these concepts. All they do is desugar to the expressions that I describe above, and any other significance (such as relation to monads) results from how the operator was defined.

Apiculture answered 9/1, 2023 at 0:46 Comment(4)
"let() does not appear anywhere in valid OCaml syntax". What about let () = print_string "hello world\n"Elroyels
D'oh, the way the asker had written let() without the spaces threw me off. I have corrected my answer.Apiculture
Thank you Alan for the detailed response. Yep, i missed the space - will correct it.Michelle
@BangarRaju Not putting a space is still syntactically correct because OCaml can tell that let and ( are distinct tokens. It's just the common style to put a space, so when I saw the code without a space, I didn't realize what I was looking at and thought it was some nonexistent construct.Apiculture
F
4

Consider the following code from the OCaml page on let operators.

let ( let* ) o f =
  match o with
  | None -> None
  | Some x -> f x

let return x = Some x

If we create a very simply map:

module M = Map.Make (Int)

let m = M.(empty |> add 1 4 |> add 2 3 |> add 3 7)

If we wanted to write a function that takes a map and two keys and adds the values at those keys, returning int option, we might write:

let add_values m k1 k2 =
  match M.find_opt k1 m with
  | None -> None
  | Some v1 ->
    match M.find_opt k2 m with
    | None -> None
    | Some v2 ->
      Some (v1 + v2)

Now, of course there are multiple ways of defining this. We could:

let add_values m k1 k2 =
  match (M.find_opt k1 m, M.find_opt k2 m) with
  | (None, _) | (_, None) -> None
  | (Some v1, Some v2) -> Some (v1 + v2)

Or take advantage of exceptions:

let add_values m k1 k2 =
  try 
    Some (M.find k1 m + M.find k2 m)
  with
  | Not_found -> None

Let operators let us write:

let add_values m k1 k2 =
  let* v1 = M.find_opt k1 m in
  let* v2 = M.find_opt k2 m in
  return (v1 + v2)
Fabrizio answered 9/1, 2023 at 1:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.