45

Possible Duplicates:
Why are functions in OCaml/F# not recursive by default?
What's the reason of 'let rec' for impure functional language OCaml?

OCaml uses let to define a new function, or let rec to define a function that is recursive. Why does it need both of these - couldn't we just use let for everything?

For example, to define a non-recursive successor function and recursive factorial in OCaml (actually, in the OCaml interpreter) I might write

let succ n = n + 1;;

let rec fact n =
    if n = 0 then 1 else n * fact (n-1);;

Whereas in Haskell (GHCI) I can write

let succ n = n + 1

let fact n =
    if n == 0 then 1 else n * fact (n-1)

Why does OCaml distinguish between let and let rec? Is it a performance issue, or something more subtle?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Chris Taylor
  • 46,912
  • 15
  • 110
  • 154

1 Answers1

43

Well, having both available instead of only one gives the programmer tighter control on the scope. With let x = e1 in e2, the binding is only present in e2's environment, while with let rec x = e1 in e2 the binding is present in both e1 and e2's environments.

(Edit: I want to emphasize that it is not a performance issue, that makes no difference at all.)

Here are two situations where having this non-recursive binding is useful:

  • shadowing an existing definition with a refinement that use the old binding. Something like: let f x = (let x = sanitize x in ...), where sanitize is a function that ensures the input has some desirable property (eg. it takes the norm of a possibly-non-normalized vector, etc.). This is very useful in some cases.

  • metaprogramming, for example macro writing. Imagine I want to define a macro SQUARE(foo) that desugars into let x = foo in x * x, for any expression foo. I need this binding to avoid code duplication in the output (I don't want SQUARE(factorial n) to compute factorial n twice). This is only hygienic if the let binding is not recursive, otherwise I couldn't write let x = 2 in SQUARE(x) and get a correct result.

So I claim it is very important indeed to have both the recursive and the non-recursive binding available. Now, the default behaviour of the let-binding is a matter of convention. You could say that let x = ... is recursive, and one must use let nonrec x = ... to get the non-recursive binder. Picking one default or the other is a matter of which programming style you want to favor and there are good reasons to make either choice. Haskell suffers¹ from the unavailability of this non-recursive mode, and OCaml has exactly the same defect at the type level : type foo = ... is recursive, and there is no non-recursive option available -- see this blog post.

¹: when Google Code Search was available, I used it to search in Haskell code for the pattern let x' = sanitize x in .... This is the usual workaround when non-recursive binding is not available, but it's less safe because you risk writing x instead of x' by mistake later on -- in some cases you want to have both available, so picking a different name can be voluntary. A good idiom would be to use a longer variable name for the first x, such as unsanitized_x. Anyway, just looking for x' literally (no other variable name) and x1 turned a lot of results. Erlang (and all language that try to make variable shadowing difficult: Coffeescript, etc.) has even worse problems of this kind.

That said, the choice of having Haskell bindings recursive by default (rather than non-recursive) certainly makes sense, as it is consistent with lazy evaluation by default, which makes it really easy to build recursive values -- while strict-by-default languages have more restrictions on which recursive definitions make sense.

Kevin Ji
  • 10,479
  • 4
  • 40
  • 63
gasche
  • 31,259
  • 3
  • 78
  • 100
  • 3
    Nice post, yet I don't feel convinved by the "metaprogramming" thing. One could argue to write `square` as a function and when your compiler has inlining capabilities, this will then effectively behave like a macro, complete with alpha-renaming and all that. – Ingo Feb 17 '12 at 12:32
  • 3
    In addition, the shadowing issue can be most easily done with idioms like: `foo x = work (sanitized x) where work x = ....` – Ingo Feb 17 '12 at 12:35
  • 1
    @Ingo, metaprogramming is much more than just expanding generic functions. – SK-logic Feb 17 '12 at 13:07
  • @SK-logic: Perhaps this is why gasches example didn't convince me. – Ingo Feb 17 '12 at 13:10
  • @Ingo, this example is pretty obvious if you've coded in, say, Lisp. Otherwise you have to understand the basics of metaprogramming first - any real world example would be of course much more complicated than this one. – SK-logic Feb 17 '12 at 13:16
  • @Ingo: your expansion idiom can be simplified to `foo x = (\x -> ...) (sanitized x)`; in other words, you are rediscovering the usual way to present `let x = e1 in e2` in untyped lambda-calculus as `(\x -> e2) e1`. This can also be applied in the macro case, eg. you'd desugar `SQUARE(foo)` into `(\x -> ...) foo`. I still prefer to be able to use `let` directly, and besides it makes a difference for typing -- at least, when local let *are* generalized. – gasche Feb 17 '12 at 13:17
  • @gasche You can also write `let work x = ...; foo x = work (sanitized x) ...` if you like that better. Hence I think the variable shadowing argument does not really hold water in the end. I can imagine that in the time ML was invented people thought that it was important to distinguish between recursive and not recursive lets, but, as others have observed, nowadays it is a bit surprising that one should annotate a function as recursive in general and in a functional language in particular. – Ingo Feb 17 '12 at 15:52
  • O'Caml has a powerful module system, and ability to "open" namespaces, so that their contents get into the child scope of the "open" construct. If such a namespace gets modified to contain a definition of "x", there's already "x" in scope and scope semantics of the language are wrong, we might get some scary shadowing. Isn't `let / let rec` distinction a requirement if we want to avoid that? – polkovnikov.ph Nov 25 '17 at 03:03
  • 1
    @polkovnikov.ph I think this an independent issue. The let/letrec distinction only matters for definition of the form `let (rec)? x = e` where `e` contains references to `x`, and then the question is what that reference means, either the current definition or the previous one. Adding an `open` before that definition may shadow a previous `x` binding and change the meaning of "the previous one", but it does not change the recursive or non-recursive nature of the definition -- it introduces no more confusion than adding a `let x = foo` definition right before this line. – gasche Dec 22 '17 at 10:00