git.iko.soy/lynxjs-elm/compiler
on master help sign in
Check in hints for bad recursion error
commit1832e701a5f2d0f06675ecea2ce8c49c8e0cd7c3 authorEvan Czaplicki <evancz@users.noreply.github.com> dateMon 31 Oct 2016, 19:27 (10 years ago) parent704f726
1 file changed+137
hints/bad-recursion.md+137
hints/bad-recursion.mdnew file+137
@@ -0,0 +1,137 @@
1+# Hints for Bad Recursion
2+
3+There are two problems that will lead you here, both of them pretty tricky:
4+
5+ 1. [**No Mutation**](#no-mutation) &mdash; Defining values in Elm is slightly different than defining values in languages like JavaScript.
6+
7+ 2. [**Tricky Recursion**](#tricky-recursion) &mdash; 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.
8+
9+
10+## No Mutation
11+
12+Languages 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?
13+
14+Well, what does it mean with functions? In Elm, we write recursive functions like this:
15+
16+```elm
17+factorial : Int -> Int
18+factorial n =
19+ if n <= 0 then 1 else n * factorial (n - 1)
20+```
21+
22+One 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.
23+
24+The thing that surprises newcomers is that recursion works the same way values too. So take the following definition:
25+
26+```elm
27+x = x + 1
28+```
29+
30+We 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.
31+
32+The fix is usually to just give the new value a new name. So you could rewrite it to:
33+
34+```elm
35+x1 = x + 1
36+```
37+
38+Now `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!
39+
40+
41+## Tricky Recursion
42+
43+Now, 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:
44+
45+```elm
46+type alias Comment =
47+ { message : String
48+ , upvotes : Int
49+ , downvotes : Int
50+ , responses : Responses
51+ }
52+
53+type Responses = Responses (List Comment)
54+```
55+
56+> Some of you may have recently run into this definition in the [hints for recursive aliases](recursive-alias.md)!
57+
58+And from there, you may want to be able to turn all the comments 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+
60+```elm
61+import Json.Decode as Decode exposing (Decoder)
62+
63+decodeComment : Decoder Comment
64+decodeComment =
65+ Decode.map4 Comment
66+ (Decode.field "message" Decode.string)
67+ (Decode.field "upvotes" Decode.int)
68+ (Decode.field "downvotes" Decode.int)
69+ (Decode.field "responses" decodeResponses)
70+
71+-- PROBLEM
72+decodeResponses : Decoder Responses
73+decodeResponses =
74+ Decode.map Responses (Decode.list decodeComment)
75+```
76+
77+The 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!
78+
79+In 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+
81+```elm
82+import Json.Decode as Decode exposing (Decoder)
83+
84+decodeComment : Decoder Comment
85+decodeComment =
86+ Decode.map4 Comment
87+ (Decode.field "message" Decode.string)
88+ (Decode.field "upvotes" Decode.int)
89+ (Decode.field "downvotes" Decode.int)
90+ (Decode.field "responses" decodeResponses)
91+
92+-- SOLUTION
93+decodeResponses : Decoder Responses
94+decodeResponses =
95+ Decode.map Responses (Decode.list (Decode.lazy (\_ -> decodeComment)))
96+```
97+
98+Notice 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+
100+
101+### Summary
102+
103+**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+
105+**Solution:** put the recursion behind an anonymous function.
106+
107+You can always do this by defining `lazy` in terms of `andThen`. In the case of random value generators, it would look like this:
108+
109+```elm
110+import Random exposing (Generator, andThen)
111+import Random.Extra exposing (constant)
112+
113+lazy : (() -> Generator a) -> Generator a
114+lazy delayedGenerator =
115+ constant ()
116+ |> andThen delayedGenerator
117+```
118+
119+If 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+
121+
122+## Defining “Bad” Recursion
123+
124+The 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+
126+**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+
128+```elm
129+factorial =
130+ \n -> if n <= 0 then 1 else n * factorial (n - 1)
131+```
132+
133+There 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.
134+
135+This 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 you 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).
136+
137+Anyway, 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*.