What's the reason of 'let rec' for impure functional language OCaml?

前端 未结 6 1769
梦谈多话
梦谈多话 2021-02-02 06:54

In the book Real World OCaml, the authors put why OCaml uses let rec for defining recursive functions.

OCaml distinguishes between nonrecurs

6条回答
  •  悲&欢浪女
    2021-02-02 07:53

    What are the technical reasons that enforces let rec while pure functional languages not?

    Recursiveness is a strange beast. It has a relation to purity, but it's a little more oblique than this. To be clear, you could write "alterna-Haskell" which retains its purity, its laziness but does not have recursively bound lets by default and demands some kind of rec marker just as OCaml does. Some would even prefer this.


    In essence, there are just many different kinds of "let"s possible. If we compare let and let rec in OCaml we'll see a small difference. In static formal semantics, we might write

    Γ ⊢ E : A    Γ, x : A ⊢ F : B
    -----------------------------
       Γ ⊢ let x = E in F : B
    

    which says that if we can prove in a variable environment Γ that E has type A and if we can prove in the same variable environment Γ augmented with x : A that F : B then we can prove that in the variable environment Γ let x = E in F has type B.

    The thing to watch is the Γ argument. This is just a list of ("variable name", "value") pairs like [(x, 3); (y, "hello")] and augmenting the list like Γ, x : A just means consing (x, A) on to it (sorry that the syntax is flipped).

    In particular, let's write the same formalism for let rec

    Γ, x : A ⊢ E : A    Γ, x : A ⊢ F : B
    -------------------------------------
           Γ ⊢ let rec x = E in F : B
    

    In particular, the only difference is that neither of our premises work in the plain Γ environment; both are allowed to assume the existence of the x variable.

    In this sense, let and let rec are simply different beasts.


    So what does it mean to be pure? At the strictest definition, of which Haskell doesn't even participate, we must eliminate all effects including non-termination. The only way to achieve this is to pull away our ability to write unrestricted recursion and replace it only carefully.

    There exist plenty of languages without recursion. Perhaps the most important one is the Simply Typed Lambda Calculus. In it's basic form it is regular lambda calculus but augmented with a typing discipline where types are bit like

    type ty =
      | Base
      | Arr of ty * ty
    

    It turns out that STLC cannot represent recursion---the Y combinator, and all other fixed-point cousin combinators, cannot be typed. Thusly, STLC is not Turing Complete.

    It is however uncompromisingly pure. It achieves that purity with the bluntest of instruments, however, by completely outlawing recursion. What we'd really like is some kind of balanced, careful recursion which doesn't lead to non-termination---we'll still be Turing Incomplete, but not so crippled.

    Some languages try this game. There are clever ways of adding typed recursion back along a division between data and codata which ensures that you cannot write non-terminating functions. If you're interested, I suggest learning a bit of Coq.


    But OCaml's goal (and Haskell's as well) is not to be delicate here. Both languages are uncompromisingly Turing Complete (and therefore "practical"). So let's discuss some more blunt ways of augmenting the STLC with recursion.

    The bluntest of the bunch is to add a single built-in function called fix

    val fix : ('a -> 'a) -> 'a
    

    or, in more genuine OCaml-y notation which requires eta-expansion

    val fix : (('a -> 'b) -> ('a -> 'b)) -> ('a -> 'b)
    

    Now, remember that we're only considering a primitive STLC with fix added. We can indeed write fix (the latter one at least) in OCaml, but that's cheating at the moment. What does fix buy the STLC as a primitive?

    It turns out that the answer is: "everything". STLC + Fix (basically a language called PCF) is impure and Turing Complete. It's also simply tremendously difficult to use.


    So this is the final hurdle to jump: how do we make fix easier to work with? By adding recursive bindings!

    Already, STLC has a let construction. You can think of it as just syntax sugar:

    let x = E in F   ---->   (fun x -> F) (E)
    

    but once we've added fix we also have the power to introduce let rec bindings

    let rec x a = E in F ----> (fun x -> F) (fix (fun x a -> E))
    

    At this point it should again be clear: let and let rec are very different beasts. They embody different levels of linguistic power and let rec is a window to allow fundamental impurity through Turing Completeness and its partner-effect non-termination.


    So, at the end of the day, it's a little amusing that Haskell, the purer of the two languages, made the interesting choice of abolishing plain let bindings. That's really the only difference: there is no syntax for representing a non-recursive binding in Haskell.

    At this point it's essentially just a style decision. The authors of Haskell determined that recursive bindings were so useful that one might as well assume that every binding is recursive (and mutually so, a can of worms ignored in this answer so far).

    On the other hand, OCaml gives you to ability to be totally explicit about the kind of binding you choose, let or let rec!

提交回复
热议问题