Skip to content

Commit 86583d0

Browse files
committed
link to actual documentation for trickiest bad recursion case
Fix #1880
1 parent 6f3a47e commit 86583d0

File tree

2 files changed

+26
-29
lines changed

2 files changed

+26
-29
lines changed

compiler/src/Generate/JavaScript.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ generateCycle mode (Opt.Global home _) names values functions =
257257
"Some top-level definitions from `" <> Name.toBuilder (ModuleName._module home) <> "` are causing infinite recursion:\\n"
258258
<> drawCycle names
259259
<> "\\n\\nThese errors are very tricky, so read "
260-
<> B.stringUtf8 (D.makeNakedLink "halting-problem")
260+
<> B.stringUtf8 (D.makeNakedLink "bad-recursion")
261261
<> " to learn how to fix it!"
262262
]
263263

hints/bad-recursion.md

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ There are two problems that will lead you here, both of them pretty tricky:
55

66
1. [**No Mutation**](#no-mutation) &mdash; Defining values in Elm is slightly different than defining values in languages like JavaScript.
77

8-
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+
2. [**Tricky Recursion**](#tricky-recursion) &mdash; Sometimes you need to define recursive values when creating generators, decoders, and parsers. A common case is a JSON decoder a discussion forums where a comment may have replies, which may have replies, which may have replies, etc.
99

1010

1111
## No Mutation
@@ -51,10 +51,11 @@ type alias Comment =
5151
, responses : Responses
5252
}
5353

54-
type Responses = Responses (List Comment)
54+
type Responses =
55+
Responses (List Comment)
5556
```
5657

57-
You 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:
58+
You 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:
5859

5960
```elm
6061
import Json.Decode as Decode exposing (Decoder)
@@ -91,46 +92,42 @@ decodeComment =
9192
-- SOLUTION
9293
decodeResponses : Decoder Responses
9394
decodeResponses =
94-
Decode.map Responses (Decode.list (Decode.lazy (\\_ -> decodeComment)))
95+
Decode.map Responses (Decode.list (Decode.lazy (\_ -> decodeComment)))
9596
```
9697

97-
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!
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!
9899

100+
This saves us from expanding the value infinitely. Instead we only expand the value if we need to.
99101

100-
### Summary
102+
> **Note:** The same kind of logic can be applied to tasks, random value generators, and parsers. Use `lazy` or `andThen` to make sure a recursive value is only expanded if needed.
101103
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.
103104

104-
**Solution:** put the recursion behind an anonymous function.
105+
## Understanding “Bad Recursion”
105106

106-
You can always do this by defining `lazy` in terms of `andThen`. In the case of random value generators, it would look like this:
107+
The compiler tries to detect 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?
107108

108-
```elm
109-
import Random exposing (Generator, andThen)
110-
import Random.Extra exposing (constant)
109+
**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:
111110

112-
lazy : (() -> Generator a) -> Generator a
113-
lazy delayedGenerator =
114-
constant ()
115-
|> andThen delayedGenerator
111+
```elm
112+
factorial =
113+
\n -> if n <= 0 then 1 else n * factorial (n - 1)
116114
```
117115

118-
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!
119-
120-
121-
## Defining “Bad” Recursion
116+
There is technically a lambda between the definition and the use, so it is okay! The same is true with the good version of `decodeComment`. There is a lambda between the definition and the use. As long as there is a lambda before you get back to yourself, the compiler will let it through.
122117

123-
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?
124-
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:
118+
**This rule is nice, but it does not catch everything.** It is pretty easy to write a definition where the recursion is hidden behind a lambda, but it still immediately expands forever:
126119

127120
```elm
128-
factorial =
129-
\\n -> if n <= 0 then 1 else n * factorial (n - 1)
121+
x =
122+
(\_ -> x) () + 1
130123
```
131124

132-
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.
125+
This follows the rules, but it immediately expands until our program runs out of stack space. It leads to a runtime error as soon as you start your program. It is nice to fail fast, but why not have the compiler detect this as well? It turns out this is much harder than it sounds!
126+
127+
This is called [the halting problem](https://en.wikipedia.org/wiki/Halting_problem) in computer science. Computational theorists were asking:
128+
129+
> Can we determine if a program will finish running (i.e. halt) or if it will continue to run forever?
133130
134-
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 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).
131+
It turns out that Alan Turing wrote a proof in 1936 showing that (1) in some cases you just have to check by running the program and (2) this check will take forever for programs that do not halt!
135132

136-
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*.
133+
**So we cannot solve the halting problem *in general*, but our simple rule about lambdas can detect the majority of bad cases *in practice*.**

0 commit comments

Comments
 (0)