Meaning of "let" and "in"

Can i consider “let” the beginning of a definition and “in” the end of a definition?
And a program just a list of definitions ?
PS1 : I have no lines ending on ; or ;;
PS2: It would be nice if i could remove the boilerplate let in.

A program (or more precisely, an OCaml module) is a sequence of toplevel definitions of the form let <identifier> = <expression> ;; (the ;; is optional). An expression can be many things, including the form let <identifier> = <expression> in <expression>, which binds <identifier> to the result of the first expression in the second one.

Some people consider the syntax let ... in heavyweight, but it’s unlikely to change now. On the bright side, it is close to the natural English “let x equal 42 in the following expression…”. It is also helpful to have a clear distinction between giving a name to the result of some expression (let x = 42 in ...) and modifying a mutable value (x := 42).

2 Likes

To be precise, this is not quite true as toplevel definitions can also be type, class or module definitions, and a few other things; toplevel expressions are also allowed.

(* Toplevel binding *)
let x = ref 42 ;;

(* Toplevel expression *)
Printf.printf "%d\n" !x ;;

(* Dummy binding. Effect is the same as above, but this checks that the
   expression has type unit. It also allows to remove `;;` without the parser
   complaining about ambiguous syntax. *)
let () = Printf.printf "%d\n" !x

You almost always can, by skipping binding values to names and passing them to function calls directly. If you mean you’d prefer the more pythonic “x = 3” sort of binding, i think this is just a matter of syntax difference, not an inability to get rid of boilerplate.

1 Like

This is more than a matter of syntax. It is a matter of specification: OCaml makes explicit the evaluation hierarchy with let … and … in. When you read the code, you just know which evaluation requires another one, and all this explicit stuff is the strength of the language.

(and although the compiler does take the point, one could imagine a future evolution that would compile differently the sequence)

3 Likes

let <lhs> = <rhs> in has advantages that <lhs2> = <rhs2> doesn’t have. Some of them:

  • let is a visual queue that something is being defined
  • the form is whitespace independent, which is much better than whitespace dependent languages for FP. E.g. In Scala, newlines are a big problem for its syntax and semantics.
  • let .. in and let .. and .. and let rec .. and .. has different meanings - which are useful for informing intention through limiting possibilities.
  • <lhs> allows pattern-matching
  • allows reuse of = for equality
5 Likes

I’ve always found Standard ML’s approach a tad more user-friendly and easier on the eye:

let
  val a = 1
  val b = 2
in
  a + b
end

But at the end of the day, it’s probably something a developer can get used to with quite easily.

I’m curious about this perspective because, as strange as it may sound, the distinction between fun and value bindings has seemed to be a rather make-or-break issue for me in terms of the programming language usability.

I have wanted to try out mlton and other SML derivatives, but have always found myself unable to leave OCaml’s particular flavour of syntax.

In particular, I find the OCaml style of let bindings to be particularly important, as they mean that converting a value binding to a function to only be a matter of a few keypresses, thereby allowing for a faster end-user experience when it comes to introducing abstractions (just specify which variables in an expression you want to be parametric, and suddenly your value binding transparently becomes a reusable function.

3 Likes

I have an interesting example of values being turned into functions, where I was defining the html of my website using functions passing different values and configurations around. At one point I made one of the values being passed, into a locally defined closure instead, to be able to configure it in a certain way - which made the compiler tell me at all places of use, deeply nested in the call graph, that I needed to pass an argument (all use-sites needed to pass different things) (:

So the obvious transformation of values to functions can be really elegant mixed with type inference

1 Like

This is actually even more important: For a clear semantic we need some way to distinguish declaration and assignment: What does a = 42 mean? Is it:

  • Creating a new binding a with the value 42?
  • Assigning the existing a with the value 42?

Python got this wrong, where the compiler doesn’t know and where it is ambiguous would exit with an UndefinedLocalError. So they had to add global and nonlocal as declaration to tell the compiler what the meaning of the subsequent a = 42 is.

So yes, it is less writing, but the let is imporant. One could attempt to avoid the in and make it implied (something like let a = b and b = 42 a) but it can be somewhat confusing.

6 Likes