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.
|