on 2f4ea95 help sign in
elk/docs/hints/bad-recursion.md
135 lines 6.9 kB history raw
1# Hints for Bad Recursion
3There are two problems that will lead you here, both of them pretty tricky:
5 1. [**No Mutation**](#no-mutation) — Defining values in Elm is slightly different than defining values in languages like JavaScript.
7 2. [**Tricky Recursion**](#tricky-recursion) — Sometimes you need to define recursive values. A common case is JSON decoders for things like discussion forums, where a comment may have replies, which may have replies, etc.
10## No Mutation
12Languages like JavaScript let you “reassign” variables. When you say `x = x + 1` it means: whatever `x` was pointing to, have it point to `x + 1` instead. This called *mutating* a variable. All values are immutable in Elm, so reassigning variables does not make any sense! Okay, so what *should* `x = x + 1` mean in Elm?
14Well, what does it mean with functions? In Elm, we write recursive functions like this:
16```elm
17factorial : Int -> Int
18factorial n =
19 if n <= 0 then 1 else n * factorial (n - 1)
20```
22One cool thing about Elm is that whenever you see `factorial 3`, you can always replace that expression with `if 3 <= 0 then 1 else 3 * factorial (3 - 1)` and it will work exactly the same. So when Elm code gets evaluated, we will keep expanding `factorial` until the `if` produces a 1. At that point, we are done expanding and move on.
24The thing that surprises newcomers is that recursion works the same way with values too. So take the following definition:
26```elm
27x = x + 1
28```
30We are actually defining `x` in terms of itself. So it would expand out to `x = ... + 1 + 1 + 1 + 1`, trying to add one to `x` an infinite number of times! This means your program would just run forever, endlessly expanding `x`. In practice, this means the page freezes and the computer starts to get kind of warm. No good! We can detect cases like this with the compiler, so we give an error at compile time so this does not happen in the wild.
32The fix is usually to just give the new value a new name. So you could rewrite it to:
34```elm
35x1 = x + 1
36```
38Now `x` is the old value and `x1` is the new value. Again, one cool thing about Elm is that whenever you see a `factorial 3` you can safely replace it with its definition. Well, the same is true of values. Wherever I see `x1`, I can replace it with `x + 1`. Thanks to the way definitions work in Elm, this is always safe!
41## Tricky Recursion
43Now, there are some cases where you *do* want a recursive value. Say you are building a website with comments and replies. You may define a comment like this:
45```elm
46type alias Comment =
47 { message : String
48 , upvotes : Int
49 , downvotes : Int
50 , responses : Responses
51 }
53type Responses = Responses (List Comment)
54```
56You may have run into this definition in the [hints for recursive aliases](recursive-alias.md)! Anyway, once you have comments, you may want to turn them into JSON to send back to your server or to store in your database or whatever. So you will probably write some code like this:
58```elm
59import Json.Decode as Decode exposing (Decoder)
61decodeComment : Decoder Comment
62decodeComment =
63 Decode.map4 Comment
64 (Decode.field "message" Decode.string)
65 (Decode.field "upvotes" Decode.int)
66 (Decode.field "downvotes" Decode.int)
67 (Decode.field "responses" decodeResponses)
69-- PROBLEM
70decodeResponses : Decoder Responses
71decodeResponses =
72 Decode.map Responses (Decode.list decodeComment)
73```
75The problem is that now `decodeComment` is defined in terms of itself! To know what `decodeComment` is, I need to expand `decodeResponses`. To know what `decodeResponses` is, I need to expand `decodeComment`. This loop will repeat endlessly!
77In this case, the trick is to use `Json.Decode.lazy` which delays the evaluation of a decoder until it is needed. So the valid definition would look like this:
79```elm
80import Json.Decode as Decode exposing (Decoder)
82decodeComment : Decoder Comment
83decodeComment =
84 Decode.map4 Comment
85 (Decode.field "message" Decode.string)
86 (Decode.field "upvotes" Decode.int)
87 (Decode.field "downvotes" Decode.int)
88 (Decode.field "responses" decodeResponses)
90-- SOLUTION
91decodeResponses : Decoder Responses
92decodeResponses =
93 Decode.map Responses (Decode.list (Decode.lazy (\_ -> decodeComment)))
94```
96Notice that in `decodeResponses`, we hide `decodeComment` behind an anonymous function. Elm cannot evaluate an anonymous function until it is given arguments. So it allows us to delay evaluation until it is needed. If there are no comments, we will not need to expand it!
99### Summary
101**Situation:** You want a recursive JSON decoder, random value generator, or task. These are all cases where a *value* represents some sort of computation.
103**Solution:** put the recursion behind an anonymous function.
105You can always do this by defining `lazy` in terms of `andThen`. In the case of random value generators, it would look like this:
107```elm
108import Random exposing (Generator, andThen)
109import Random.Extra exposing (constant)
111lazy : (() -> Generator a) -> Generator a
112lazy delayedGenerator =
113 constant ()
114 |> andThen delayedGenerator
115```
117If you are in some situation where this will not work, I am curious to hear about it. I would recommend asking around in [the Elm slack](http://elmlang.herokuapp.com/) first, and opening an issue here if it seems to be the real deal!
120## Defining “Bad” Recursion
122The compiler will yell at you for *bad* recursion, but how does it know the difference between good and bad situations? Writing `factorial` is fine, but writing `x = x + 1` is not. One version of `decodeComment` was bad, but the other was fine. What is the rule?
124**Elm will allow recursive definitions as long as there is at least one lambda before you get back to yourself.** So if we write `factorial` without any pretty syntax, it looks like this:
126```elm
127factorial =
128 \n -> if n <= 0 then 1 else n * factorial (n - 1)
129```
131There is technically a lambda between the definition and the use! The same is true with the good version of `decodeComment`. As long as there is a lambda before you get back to yourself, the compiler will let it through.
133This rule is nice, but it does not catch *everything*. Pathological cases like `x = (\_ -> x) () + 1` can make it through. Why not rule these out as well though?! Well, because it is impossible! And not the normal kind of impossible where your cable bill is messed up and the fifth time you call it turns out it *can* be corrected. This is the &ldquo;we proved it with math, it’s called [the halting problem](https://en.wikipedia.org/wiki/Halting_problem)&rdquo; kind of impossible. Impossible [like this](https://www.youtube.com/watch?v=nlD9JYP8u5E).
135Anyway, I just thought it was cool that we cannot solve the halting problem *in general*, but a simple rule about lambdas can detect the majority of bad cases *in practice*.