File: recursive-alias.md

package info (click to toggle)
elm-compiler 0.19.1-4
  • links: PTS
  • area: main
  • in suites: forky, sid, trixie
  • size: 5,296 kB
  • sloc: haskell: 35,930; javascript: 5,404; sh: 82; xml: 27; python: 26; makefile: 13
file content (162 lines) | stat: -rw-r--r-- 4,356 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162

# Hints for Recursive Type Aliases

At the root of this issue is the distinction between a `type` and a `type alias`.


## What is a type alias?

When you create a type alias, you are just creating a shorthand to refer to an existing type. So when you say the following:

```elm
type alias Time = Float

type alias Degree = Float

type alias Weight = Float
```

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:

```elm
add : Time -> Degree -> Weight
add time degree =
  time + degree
```

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:

```elm
type alias Person =
  { name : String
  , age : Int
  , height : Float
  }
```

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.


## Recursive type aliases?

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:

```elm
type alias Comment =
  { message : String
  , upvotes : Int
  , downvotes : Int
  , responses : List Comment
  }
```

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.

```elm
  { message : String
  , upvotes : Int
  , downvotes : Int
  , responses :
      List
        { message : String
        , upvotes : Int
        , downvotes : Int
        , responses :
            List
              { message : String
              , upvotes : Int
              , downvotes : Int
              , responses : List ...
              }
        }
  }
```

The compiler cannot deal with values like this. It would just keep expanding forever.


## Recursive types!

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:

```elm
type List
    = Empty
    | Node Int List
```

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 that do not explode into infinity.

So let's return to wanting to represent a `Comment` that may have responses. There are a couple ways to do this:


### Obvious, but kind of annoying

```elm
type Comment =
   Comment
      { message : String
      , upvotes : Int
      , downvotes : Int
      , responses : List Comment
      }
```

Now lets say you want to register an upvote on a comment:

```elm
upvote : Comment -> Comment
upvote (Comment comment) =
  Comment { comment | upvotes = 1 + comment.upvotes }
```

It is kind of annoying that we now have to unwrap and wrap the record to do anything with it.


### Less obvious, but nicer

```elm
type alias Comment =
  { message : String
  , upvotes : Int
  , downvotes : Int
  , responses : Responses
  }

type Responses = Responses (List Comment)
```

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:

```elm
upvote : Comment -> Comment
upvote comment =
  { comment | upvotes = 1 + comment.upvotes }
```

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 recursive. In practice, this means you will do less unwrapping which is nice.


## Mutually recursive type aliases

It is also possible to build type aliases that are *mutually* recursive. That might be something like this:

```elm
type alias Comment =
  { message : String
  , upvotes : Int
  , downvotes : Int
  , responses : Responses
  }

type alias Responses =
  { sortBy : SortBy
  , responses : List Comment
  }

type SortBy = Time | Score | MostResponses
```

When you try to expand `Comment` you have to expand `Responses` which needs to expand `Comment` which needs to expand `Responses`, etc.

So this is just a fancy case of a self-recursive type alias. The solution is the same. Somewhere in that cycle, you need to define an actual `type` to end the infinite expansion.