on aaa2d65 help sign in
elk/hints/bad-recursion.md
136 lines 6.9 kB history raw
2# Hints for Bad Recursion
4There are two problems that will lead you here, both of them pretty tricky:
6 1. [**No Mutation**](#no-mutation) — Defining values in Elm is slightly different than defining values in languages like JavaScript.
8 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.
11## No Mutation
13Languages 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?
15Well, what does it mean with functions? In Elm, we write recursive functions like this:
17```elm
18factorial : Int -> Int
19factorial n =
20 if n <= 0 then 1 else n * factorial (n - 1)
21```
23One 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.
25The thing that surprises newcomers is that recursion works the same way with values too. So take the following definition:
27```elm
28x = x + 1
29```
31We 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.
33The fix is usually to just give the new value a new name. So you could rewrite it to:
35```elm
36x1 = x + 1
37```
39Now `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!
42## Tricky Recursion
44Now, 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:
46```elm
47type alias Comment =
48 { message : String
49 , upvotes : Int
50 , downvotes : Int
51 , responses : Responses
52 }
54type Responses = Responses (List Comment)
55```
57You may have run into this definition in the [hints for recursive aliases](recursive-alias)! 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:
59```elm
60import Json.Decode as Decode exposing (Decoder)
62decodeComment : Decoder Comment
63decodeComment =
64 Decode.map4 Comment
65 (Decode.field "message" Decode.string)
66 (Decode.field "upvotes" Decode.int)
67 (Decode.field "downvotes" Decode.int)
68 (Decode.field "responses" decodeResponses)
70-- PROBLEM
71decodeResponses : Decoder Responses
72decodeResponses =
73 Decode.map Responses (Decode.list decodeComment)
74```
76The 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!
78In 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:
80```elm
81import Json.Decode as Decode exposing (Decoder)
83decodeComment : Decoder Comment
84decodeComment =
85 Decode.map4 Comment
86 (Decode.field "message" Decode.string)
87 (Decode.field "upvotes" Decode.int)
88 (Decode.field "downvotes" Decode.int)
89 (Decode.field "responses" decodeResponses)
91-- SOLUTION
92decodeResponses : Decoder Responses
93decodeResponses =
94 Decode.map Responses (Decode.list (Decode.lazy (\\_ -> decodeComment)))
95```
97Notice 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!
100### Summary
102**Situation:** You want a recursive JSON decoder, random value generator, or task. These are all cases where a *value* represents some sort of computation.
104**Solution:** put the recursion behind an anonymous function.
106You can always do this by defining `lazy` in terms of `andThen`. In the case of random value generators, it would look like this:
108```elm
109import Random exposing (Generator, andThen)
110import Random.Extra exposing (constant)
112lazy : (() -> Generator a) -> Generator a
113lazy delayedGenerator =
114 constant ()
115 |> andThen delayedGenerator
116```
118If 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!
121## Defining “Bad” Recursion
123The 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?
125**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:
127```elm
128factorial =
129 \\n -> if n <= 0 then 1 else n * factorial (n - 1)
130```
132There 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.
134This 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).
136Anyway, 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*.