on master help sign in
Add full hints for recursive type aliases

Show a typical example and a few ways to represent it.

commit44b86a1545a2392669d0750c343ff4df2c39e6dd authorEvan Czaplicki <info@elm-lang.org> dateTue 27 Oct 2015, 16:54 (11 years ago) parent90dc847
2 files changed+167−5
hints/recursive-alias.md+161 src/Reporting/Error/Canonicalize.hs+6−5
hints/recursive-alias.mdnew file+161
@@ -0,0 +1,161 @@
1+# Hints for Recursive Type Aliases
2+
3+At the root of this issue is the distinction between a `type` and a `type alias`.
4+
5+
6+## What is a type alias?
7+
8+When you create a type alias, you are just creating a shorthand to refer to an existing type. So when you say the following:
9+
10+```elm
11+type alias Time = Float
12+
13+type alias Degree = Float
14+
15+type alias Weight = Float
16+```
17+
18+You have not created any *new* types, you just made some alternate names for `Float`. You can write down things like this and it'll work fine:
19+
20+```elm
21+add : Time -> Degree -> Weight
22+add time degree =
23+ time + degree
24+```
25+
26+This is kind of a weird way to use type aliases though. The typical usage would be for records, where you do not want to write out the whole thing every time. Stuff like this:
27+
28+```elm
29+type alias Person =
30+ { name : String
31+ , age : Int
32+ , height : Float
33+ }
34+```
35+
36+It is much easier to write down `Person` in a type, and then it will just expand out to the underlying type when the compiler checks the program.
37+
38+
39+## Recursive type aliases?
40+
41+Okay, so lets say you have some type that may contain itself. In Elm, a common example of this is a comment that might have subcomments:
42+
43+```elm
44+type alias Comment =
45+ { message : String
46+ , upvotes : Int
47+ , downvotes : Int
48+ , responses : List Comment
49+ }
50+```
51+
52+Now remember that type *aliases* are just alternate names for the real type. So to make `Comment` into a concrete type, the compiler would start expanding it out.
53+
54+```elm
55+ { message : String
56+ , upvotes : Int
57+ , downvotes : Int
58+ , responses :
59+ List
60+ { message : String
61+ , upvotes : Int
62+ , downvotes : Int
63+ , responses :
64+ List
65+ { message : String
66+ , upvotes : Int
67+ , downvotes : Int
68+ , responses : List ...
69+ }
70+ }
71+ }
72+```
73+
74+The compiler cannot deal with values like this. It would just keep expanding forever.
75+
76+
77+## Recursive types!
78+
79+In cases where you want a recursive type, you need to actually create a brand new type. This is what the `type` keyword is for. A simple example of this can be seen when defining a linked list:
80+
81+```elm
82+type List
83+ = Empty
84+ | Node Int List
85+```
86+
87+No matter what, the type of `Node n xs` is going to be `List`. There is no expansion to be done. This means you can represent recursive structures with types do not explode into infinity.
88+
89+So let's return to wanting to represent a `Comment` that may have responses. There are a couple ways to do this:
90+
91+
92+### Obvious, but kind of annoying
93+
94+```elm
95+type Comment =
96+ Comment
97+ { message : String
98+ , upvotes : Int
99+ , downvotes : Int
100+ , responses : List Comment
101+ }
102+```
103+
104+Now lets say you want to register an upvote on a comment:
105+
106+```elm
107+upvote : Comment -> Comment
108+upvote (Comment comment) =
109+ Comment { comment | upvotes = 1 + comment.upvotes }
110+```
111+
112+It is kind of annoying that we now have to unwrap and wrap the record to do anything with it.
113+
114+
115+### Less obvious, but nicer
116+
117+```elm
118+type alias Comment =
119+ { message : String
120+ , upvotes : Int
121+ , downvotes : Int
122+ , responses : Responses
123+ }
124+
125+type Responses = Responses (List Comment)
126+```
127+
128+In this world, we introduce the `Responses` type to capture the recursion, but `Comment` is still an alias for a record. This means the `upvote` function looks nice again:
129+
130+```elm
131+upvote : Comment -> Comment
132+upvote comment =
133+ { comment | upvotes = 1 + comment.upvotes }
134+```
135+
136+So rather than having to unwrap a `Comment` to do *anything* to it, you only have to do some unwrapping in the cases where you are doing something recusive. In practice, this means you will do less unwrapping which is nice.
137+
138+
139+## Mutually recursive type aliases
140+
141+It is also possible to build type aliases that are *mutually* recursive. That might be something like this:
142+
143+```elm
144+type alias Comment =
145+ { message : String
146+ , upvotes : Int
147+ , downvotes : Int
148+ , responses : Responses
149+ }
150+
151+type alias Responses =
152+ { sortBy : SortBy
153+ , responses : List Comment
154+ }
155+
156+type SortBy = Time | Score | MostResponses
157+```
158+
159+When you try to expand `Comment` you have to expand `Responses` which needs to expand `Comment` which needs to expand `Responses`, etc.
160+
161+So this is just a fancy case of a self-recusive type alias. The solution is the same. Somewhere in that cycle, you need to define an actual `type` to end the infinite expansion.
src/Reporting/Error/Canonicalize.hs+6−5
@@ -182,8 +182,9 @@ toReport localizer err =
182182 , indent 4 $
183183 RenderType.decl localizer name tvars [(name, [unsafePromote tipe])]
184184 , text $
185− "This creates a brand new type that cannot be expanded. Read more about this at:\n"
186− ++ Help.hintLink "infinite-alias"
185+ "This is kind of a subtle distinction. I suggested the naive fix, but you can\n"
186+ ++ "often do something a bit nicer. So I would recommend reading more at:\n"
187+ ++ Help.hintLink "recursive-alias"
187188 ]
188189 )
189190
@@ -194,11 +195,11 @@ toReport localizer err =
194195 "This type alias is part of a mutually recursive set of type aliases."
195196 ( Help.stack
196197 [ text "The following type aliases are mutually recursive:"
197− , Help.drawCycle (map (\(_, name, _, _) -> name) aliases)
198+ , indent 4 (Help.drawCycle (map (\(_, name, _, _) -> name) aliases))
198199 , Help.reflowParagraph $
199200 "You need to convert at least one `type alias` into a `type`. This is a kind of\
200− \ subtle distinction, so definitely read how to fix it and have great code:"
201− ++ Help.hintLink "infinite-alias"
201+ \ subtle distinction, so definitely read up on this before you make a fix: "
202+ ++ Help.hintLink "recursive-alias"
202203 ]
203204 )
204205