In the book Real World OCaml, the authors put why OCaml uses let rec
for defining recursive functions.
OCaml distinguishes between nonrecurs
It's not a question of purity, it's a question of specifying what environment the typechecker should check an expression in. It actually gives you more power than you would have otherwise. For example (I'm going to write Standard ML here because I know it better than OCaml, but I believe the typechecking process is pretty much the same for the two languages), it lets you distinguish between these cases:
val foo : int = 5
val foo = fn (x) => if x = foo then 0 else 1
Now as of the second redefinition, foo
has the type int -> int
. On the other hand,
val foo : int = 5
val rec foo = fn (x) => if x = foo then 0 else 1
does not typecheck, because the rec
means that the typechecker has already decided that foo
has been rebound to the type 'a -> int
, and when it tries to figure out what that 'a
needs to be, there is a unification failure because x = foo
forces foo
to have a numeric type, which it doesn't.
It can certainly "look" more imperative, because the case without rec
allows you to do things like this:
val foo : int = 5
val foo = foo + 1
val foo = foo + 1
and now foo
has the value 7. That's not because it's been mutated, however --- the name foo has been rebound 3 times, and it just so happens that each of those bindings shadowed a previous binding of a variable named foo
. It's the same as this:
val foo : int = 5
val foo' = foo + 1
val foo'' = foo' + 1
except that foo
and foo'
are no longer available in the environment after the identifier foo
has been rebound. The following are also legal:
val foo : int = 5
val foo : real = 5.0
which makes it clearer that what's happening is shadowing of the original definition, rather than a side effect.
Whether or not it's stylistically a good idea to rebind identifiers is questionable -- it can get confusing. It can be useful in some situations (e.g. rebinding a function name to a version of itself that prints debugging output).