Skip to content

Commit 3d3934f

Browse files
Making changes according to feedback
1 parent 37b3fcb commit 3d3934f

File tree

1 file changed

+38
-36
lines changed

1 file changed

+38
-36
lines changed

pages/docs/manual/v12.0.0/generalized-algebraic-data-types.mdx

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
---
22
title: "Generalized Algebraic Data Types"
3-
description: "Generalized Algebraic Data Types in Rescript"
3+
description: "Generalized Algebraic Data Types in ReScript"
44
canonical: "/docs/manual/v12.0.0/generalized-algebraic-data-types"
55
---
66

77
# Generalized Algebraic Data Types
88

9-
Generalized Algebraic Data Types (GADTs) are an advanced feature of Rescript's type system. "Generalized" can be somewhat of a misnomer -- what they actually allow you to do is add some extra type-specificity to your variants. Using a GADT, you can give the individual cases of a variant _different_ types.
9+
Generalized Algebraic Data Types (GADTs) are an advanced feature of ReScript's type system. "Generalized" can be somewhat of a misnomer -- what they actually allow you to do is add some extra type-specificity to your variants. Using a GADT, you can give the individual cases of a variant _different_ types.
1010

1111
For a quick overview of the use cases, reach for GADTs when:
1212

13-
1. You need to distinguish between different members of a variant at the type-level
13+
1. You need to distinguish between different members of a variant at the type level.
1414
2. You want to "hide" type information in a type-safe way, without resorting to casts.
1515
3. You need a function to return a different type depending on its input.
1616

@@ -32,7 +32,7 @@ type timezone =
3232
Using this variant type, we will end up having functions like this:
3333

3434
```res example
35-
let convert_to_daylight = tz => {
35+
let convertToDaylight = tz => {
3636
switch tz {
3737
| EST => EDT
3838
| CST => CDT
@@ -43,22 +43,24 @@ let convert_to_daylight = tz => {
4343

4444
This function is only valid for a subset of our variant type's constructors but we can't handle this in a type-safe way using regular variants. We have to enforce that at runtime -- and moreover the compiler can't help us ensure we are failing only in the invalid cases. We are back to dynamically checking validity like we would in a language without static typing. If you work with a large variant type long enough, you will frequently find yourself writing repetitive catchall `switch` statements like the above, and for little actual benefit. The compiler should be able to help us here.
4545

46-
Lets see if we can find a way for the compiler to help us out with normal variants. We could define another variant type to distinguish the two kinds of timezone.
46+
Let's see if we can find a way for the compiler to help us with normal variants. We could define another variant type to distinguish the two kinds of timezone.
4747

4848
```res example
49-
type daylight_or_standard =
49+
type daylightOrStandard =
5050
| Daylight(timezone)
5151
| Standard(timezone)
5252
```
5353

5454
This has a lot of problems. For one, it's cumbersome and redundant. We would now have to pattern-match twice whenever we deal with a timezone that's wrapped up here. The compiler will force us to check whether we are dealing with daylight or standard time, but notice that there's nothing stopping us from providing invalid timezones to these constructors:
5555

5656
```res example
57-
let invalid_tz1 = Daylight(EST)
58-
let invalid_tz2 = Standard(EDT)
57+
let invalidTz1 = Daylight(EST)
58+
let invalidTz2 = Standard(EDT)
5959
```
6060

61-
Consequently, we still have to write our redundant catchall cases. We could define daylight savings time and standard time as two _separate_ types, and unify those in our `daylight_or_standard` variant. That could be a passable solution, but that makes a distinction really would like to do is implement some kind of _subtyping_ relationship. We have two _kinds_ of timezone. This is where GADTs are handy:
61+
Consequently, we still have to write our redundant catchall cases. We could define daylight savings time and standard time as two _separate_ types, and unify those in our `daylightOrStandard` variant.
62+
That could be a passable solution, but what we would really like to do is implement some kind of subtyping relationship.
63+
We have two _kinds_ of timezone. This is where GADTs are handy:
6264

6365
```res example
6466
type standard
@@ -75,7 +77,7 @@ We define our type with a type parameter. We manually annotate each constructor,
7577
but we've added another level of specificity using a type parameter. Constructors are now understood to be `standard` or `daylight` at the _type_ level. Now we can fix our function like this:
7678

7779
```res example
78-
let convert_to_daylight = tz => {
80+
let convertToDaylight = tz => {
7981
switch tz {
8082
| EST => EDT
8183
| CST => CDT
@@ -89,7 +91,7 @@ we try to return a standard timezone from this function. Actually, this seems li
8991
we still want to be able to match on all cases of the variant sometimes, and a naive attempt at this will not pass the type checker. A naive example will fail:
9092

9193
```res example
92-
let convert_to_daylight = tz => {
94+
let convertToDaylight = tz => {
9395
switch tz {
9496
| EST => EDT
9597
| CST => CDT
@@ -102,7 +104,7 @@ let convert_to_daylight = tz => {
102104
This will complain that `daylight` and `standard` are incompatible. To fix this, we need to explicitly annotate to tell the compiler to accept both:
103105

104106
```res example
105-
let convert_to_daylight : type a. timezone<a> => timezone<daylight> = // ...
107+
let convertToDaylight : type a. timezone<a> => timezone<daylight> = // ...
106108
```
107109

108110
`type a.` here defines a _locally abstract type_ which basically tells the compiler that the type parameter a is some specific type, but we don't care what it is. The cost of the extra specificity and safety that GADTs give us is that the compiler is not able to help us with type inference as much.
@@ -127,7 +129,7 @@ let foo = add(Int(1), Int(2))
127129
let bar = add(Int(1), Float(2.0)) // the compiler will complain here
128130
```
129131

130-
How does this work? The key thing is the function signature for add. The number GADT is acting as a `type witness`. We have told the compiler that the type parameter for `number` will be the same as the type we return -- both are set to `a`. So if we provide a `number<int>`, `a` equals `int`, and the function will therefore return an `int`.
132+
How does this work? The key thing is the function signature for add. The `number` GADT is acting as a _type witness_. We have told the compiler that the type parameter for `number` will be the same as the type we return -- both are set to `a`. So if we provide a `number<int>`, `a` equals `int`, and the function will therefore return an `int`.
131133

132134
We can also use this to avoid returning `option` unnecessarily. This example is adapted from Real World Ocaml, chapter 9. We create an array searching function can be configured to either raise an exception, return an `option`, or provide a `default` value depending on the behavior we want.
133135

@@ -165,26 +167,26 @@ let flexible_find:
165167

166168
## Hide and recover Type information Dynamically
167169

168-
In a very advanced case that combines many of the above techniques, we can use GADTs to selectively hide and recover type information. This helps us create more generic types.
170+
In an advanced case that combines the above techniques, we can use GADTs to selectively hide and recover type information. This helps us create more generic types.
169171
The below example defines a `num` type similar to our above addition example, but this lets us use `int` and `float` arrays
170-
interchangeably, hiding the implementation type rather than exposing it. This is similar to a regular variant. However, it is a tuple including embedding a `num_ty` and another value.
171-
`num_ty` serves as a type-witness, making it
172-
possible to recover type information that was hidden dynamically. Matching on `num_ty` will "reveal" the type of the other value in the pair.We can use this to write a generic sum function over arrays of numbers:
172+
interchangeably, hiding the implementation type rather than exposing it. This is similar to a regular variant. However, it is a tuple including embedding a `numTy` and another value.
173+
`numTy` serves as a type-witness, making it
174+
possible to recover type information that was hidden dynamically. Matching on `numTy` will "reveal" the type of the other value in the pair.We can use this to write a generic sum function over arrays of numbers:
173175

174176
```res example
175-
type rec num_ty<'a> =
176-
| Int: num_ty<int>
177-
| Float: num_ty<float>
178-
and num = Num(num_ty<'a>, 'a): num
179-
and num_array = Narray(num_ty<'a>, array<'a>): num_array
177+
type rec numTy<'a> =
178+
| Int: numTy<int>
179+
| Float: numTy<float>
180+
and num = Num(numTy<'a>, 'a): num
181+
and num_array = Narray(numTy<'a>, array<'a>): num_array
180182
181-
let add_int = (x, y) => x + y
182-
let add_float = (x, y) => x +. y
183+
let addInt = (x, y) => x + y
184+
let addFloat = (x, y) => x +. y
183185
184186
let sum = (Narray(witness, array)) => {
185187
switch witness {
186-
| Int => Num(Int, array->Array.reduce(0, add_int))
187-
| Float => Num(Float, array->Array.reduce(0., add_float))
188+
| Int => Num(Int, array->Array.reduce(0, addInt))
189+
| Float => Num(Float, array->Array.reduce(0., addFloat))
188190
}
189191
}
190192
```
@@ -211,7 +213,7 @@ module Stream = {
211213
}
212214
```
213215

214-
Not only is this quite tedious to write, and quite ugly, but we gain very little from it. The function wrappers even add performance overhead, so we are losing on almost all fronts. If we define subtypes of
216+
Not only is this quite tedious to write and quite ugly, but we gain very little in return. The function wrappers even add performance overhead, so we are losing on all fronts. If we define subtypes of
215217
Stream like `Readable` or `Writable`, which have all sorts of special interactions with the callback that jeopardize our type-safety, we are going to be in even deeper trouble.
216218

217219
Instead, we can use the same GADT technique that let us vary return type to vary the input type.
@@ -221,7 +223,7 @@ the type signature of the callback and pass this instead of a plain string.
221223
Additionally, we use some type parameters to represent the different types of Streams.
222224

223225
This example is complex, but it enforces tons of useful rules. The wrong event can never be used
224-
with the wrong callback, but it also will never be used with the wrong kind of stream. The compiler will will complain for example if we try to use a `Pipe` event with anything other than a `writable` stream.
226+
with the wrong callback, but it also will never be used with the wrong kind of stream. The compiler will complain for example if we try to use a `Pipe` event with anything other than a `writable` stream.
225227

226228
The real magic happens in the signature of `on`. Read it carefully, and then look at the examples and try to
227229
follow how the type variables are getting filled in, write it out on paper what each type variable is equal
@@ -260,35 +262,35 @@ let writer = Stream.Writable.make()
260262
let reader = Stream.Readable.make()
261263
// Types will be correctly inferred for each callback, based on the event parameter provided
262264
writer->Stream.on(Pipe, r => {
263-
Js.log("Piping has started")
265+
Console.log("Piping has started")
264266
265267
r->Stream.on(Data, chunk =>
266268
switch chunk {
267-
| Stream.Str(s) => Js.log(s)
268-
| Stream.Buf(buffer) => Js.log(buffer)
269+
| Stream.Str(s) => Console.log(s)
270+
| Stream.Buf(buffer) => Console.log(buffer)
269271
}
270272
)
271273
})
272274
273-
writer->Stream.on(End, _ => Js.log("End reached"))
275+
writer->Stream.on(End, _ => Console.log("End reached"))
274276
275277
```
276278

277-
This example is only over a tiny, imaginary subset of node's Stream API, but it shows a real-life example
279+
This example is only over a tiny, imaginary subset of Node's Stream API, but it shows a real-life example
278280
where GADTs are all but indispensable.
279281

280282
## Conclusion
281283

282-
While GADTs can make your types extra-expressive and get more safety, with great power comes great
284+
While GADTs can make your types extra-expressive and provide more safety, with great power comes great
283285
responsibility. Code that uses GADTs can sometimes be too clever for its own good. The type errors you
284286
encounter will be more difficult to understand, and the compiler sometimes requires extra help to properly
285287
type your code.
286288

287-
However, There are definite situations where GADTs are the _right_ decision
289+
However, there are definite situations where GADTs are the _right_ decision
288290
and will _simplify_ your code and help you avoid bugs, even rendering some bugs impossible. The `Stream` example above is a good example where the "simpler" alternative of using regular variants or even strings.
289291
would lead to a much more complex and error prone interface.
290292

291293
Ordinary variants are not necessarily _simple_ therefore, and neither are GADTs necessarily _complex_.
292294
The choice is rather which tool is the right one for the job. When your logic is complex, the highly expressive nature of GADTs can make it simpler to capture that logic.
293295
When your logic is simple, it's best to reach for a simpler tool and avoid the cognitive overhead.
294-
The only way to get good at identifying which the situation calls for is to try out
296+
The only way to get good at identifying which tool to use in a given situation is to practice and experiment with both.

0 commit comments

Comments
 (0)