Skip to content

Commit

Permalink
Edits to ADT chapter
Browse files Browse the repository at this point in the history
- Fix typos
- Reorganize sections
  • Loading branch information
noelwelsh committed Oct 2, 2023
1 parent 067da4c commit 711984f
Show file tree
Hide file tree
Showing 6 changed files with 37 additions and 26 deletions.
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ lazy val pages = List(
"adt/scala.md",
"adt/structural-recursion.md",
"adt/structural-corecursion.md",
"adt/applications.md",
"adt/algebra.md",
"adt/conclusions.md",

"type-classes/index.md",
Expand Down
5 changes: 5 additions & 0 deletions src/pages/adt/algebra.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## The Algebra of Algebraic Data Types

Algebra of algebraic data types

Exponential types, quotient types.
1 change: 1 addition & 0 deletions src/pages/adt/applications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
## Applications of Algebraic Data Types
19 changes: 5 additions & 14 deletions src/pages/adt/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

In this section we'll see our first example of a programming strategy: **algebraic data types**. Any data we can describe using logical ands and logical ors is an algebraic data type. Once we recognize an algebraic data type we get three things for free:

- the Scala representation of the data; and
- a **structural recursion** skeleton to transform the algebraic data type into any other type.
- the Scala representation of the data;
- a **structural recursion** skeleton to transform the algebraic data type into any other type; and
- a **structural co-recursion** skeleton to construct the algebraic data type from any other type.

The key point is this: from an implementation independent representation of data we can automatically derive most of the interesting implementation specific parts of working with that data.

We'll start with some examples of data, from which we'll extract the common structure that motivates algebraic data types. We then look at their representation in Scala 2 and Scala 3. We'll then turn to structural recursion, to transform algebraic data types, and structural co-recursion to construct them. We'll finish by looking at the algebra of algebraic data types, which is interesting but not essential.
We'll start with some examples of data, from which we'll extract the common structure that motivates algebraic data types. We will then look at their representation in Scala 2 and Scala 3. We'll then turn to structural recursion for transforming algebraic data types, and structural co-recursion for constructing them. We'll finish by looking at the algebra of algebraic data types, which is interesting but not essential.



Expand Down Expand Up @@ -40,15 +40,6 @@ So algebraic data types consist of sum and product types.

## Closed Worlds

Algebraic data types are closed worlds, which means they cannot be extended after that fact. In practical terms this means we have to modify the source code where we define the algebraic data type if we want to add or remove elements.
Algebraic data types are closed worlds, which means they cannot be extended after they have been defined. In practical terms this means we have to modify the source code where we define the algebraic data type if we want to add or remove elements.

The closed world property is important because it gives us some guarantees we would not otherwise have. In particular, it allows the compiler to check, when we use an algebraic data type, that we handle all possible cases and alert us if we don't. This is known as **exhaustivity checking**. This is an example of how functional programming prioritizes reasoning about code---in this case automated reasoning by the compiler---over other properties such as extensibility.


## Applications of Algebraic Data Types

## The Algebra of Algebraic Data Types

Algebra of algebraic data types

Exponential types, quotient types.
The closed world property is important because it gives us guarantees we would not otherwise have. In particular, it allows the compiler to check, when we use an algebraic data type, that we handle all possible cases and alert us if we don't. This is known as **exhaustivity checking**. This is an example of how functional programming prioritizes reasoning about code---in this case automated reasoning by the compiler---over other properties such as extensibility.
22 changes: 17 additions & 5 deletions src/pages/adt/scala.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
## Algebraic Data Types in Scala

Now we know about algebraic data types we can turn to their representation in Scala. The important point here is that the translation to Scala is entirely determined by the structure of the data. No thinking is required. In other words, the work is in finding the structure of the data that best represents the problem at hand. Work out the structure of the data and the code directly follows from it.
Now we know what algebraic data types are, we can turn to their representation in Scala. The important point here is that the translation to Scala is entirely determined by the structure of the data; no thinking is required! This means the work is in finding the structure of the data that best represents the problem at hand. Work out the structure of the data and the code directly follows from it.

As algebraic data types are defined in terms of logical ands and logical ors, to represent algebraic data types in Scala we must know how to represent these two concepts in Scala. Scala 3 simplifies the representation of algebraic data types compared to Scala 2, so we'll look at each separately.
As algebraic data types are defined in terms of logical ands and logical ors, to represent algebraic data types in Scala we must know how to represent these two concepts in Scala. Scala 3 simplifies the representation of algebraic data types compared to Scala 2, so we'll look at each language version separately.

I'm assuming that you're familiar with the language features we use to represent algebraic data types in Scala, but not with their correspondence to algebraic data types.

Expand Down Expand Up @@ -65,7 +65,7 @@ final case class C() extends A

Scala 2 has several little tricks to defining algebraic data types.

Firstly, instead of using a `sealed abstract class` you can use a `sealed trait`. There isn't much practical difference between the two. When teaching I'll often use `sealed trait` to avoid having to introduce `abstract class`. I believe `sealed abstract class` has slightly better performance and Java interoperability but I haven't tested this. I also think `sealed abstract class` is closer, semantically, to the meaning of a sum type.
Firstly, instead of using a `sealed abstract class` you can use a `sealed trait`. There isn't much practical difference between the two. When teaching beginners I'll often use `sealed trait` to avoid having to introduce `abstract class`. I believe `sealed abstract class` has slightly better performance and Java interoperability but I haven't tested this. I also think `sealed abstract class` is closer, semantically, to the meaning of a sum type.

For extra style points we can `extend Product with Serializable` from `sealed abstract class`. Compare the reported types below with and without this little addition.

Expand All @@ -77,8 +77,9 @@ final case class B() extends A
final case class C() extends A
```

```scala mdoc
```scala mdoc:silent
val list = List(B(), C())
// list: List[A extends Product with Serializable] = List(B(), C())
```

Notice how the type of `list` includes `Product` and `Serializable`.
Expand All @@ -97,13 +98,15 @@ val list = List(B(), C())

Much easier to read!

You'll only see this in Scala 2. Scala 3 has the concept of **transparent traits**, which aren't reported in inferred types, so you'll see the same output in Scala 3 no matter whether you add `Product` and `Serializable` or not.

Finally, if a logical and holds no data we can use a `case object` instead of a `case class`. For example, if we're defining some type `A` that holds no data we can just write

```scala mdoc:silent
case object A
```

Note there is no need to mark the `case object` as `final`, as objects cannot be extended.
There is no need to mark the `case object` as `final`, as objects cannot be extended.


### Examples
Expand Down Expand Up @@ -183,3 +186,12 @@ final case class Line(end: Point) extends Action
final case class Curve(cp1: Point, cp2: Point, end: Point) extends Action
final case class Move(end: Point) extends Action
```


### Representing ADTs in Scala 3

We've seen that the Scala 3 representation of algebraic data types, using `enum`, is more compact than the Scala 2 representation. However the Scala 2 representation is still avaiable. Should you ever use the Scala 2 representation in Scala 3? There are a few cases where you may want to:

- Scala 3's doesn't currently support nested `enums` (`enums` within `enums`). This may change in the future, but right now it can be more convenient to use the Scala 2 representation to express this without having to convert to disjunctive normal form.

- Scala 2's representation can express things that are almost, but not quite, algebraic data types. For example, if you define a method on an `enum` you must be able to define it for all the members of the `enum`. Sometimes you want a case of an `enum` to have methods that are only defined for that case. If so, you'll need to use the Scala 2 representation instead.
14 changes: 7 additions & 7 deletions src/pages/adt/structural-recursion.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ Structural recursion tells us how to transform an algebraic data types into any
Given an algebraic data type, the transformation can be implemented using structural recursion.

Just like with algebraic data types, there is distinction between the concept of structural recursion and the implementation in Scala.
In particular, there are two ways structural recursion can be implemented in Scala: via pattern matching or via dynamic dispatch.
We'll look at both in turn.
This is more obvious because there are two ways to implement structural recursion in Scala: via pattern matching or via dynamic dispatch.
We'll look at each in turn.


### Pattern Matching
Expand Down Expand Up @@ -87,7 +87,7 @@ enum MyList[A] {
}
```

Our step is to recognize that `map` can be written using a structural recursion.
Our first step is to recognize that `map` can be written using a structural recursion.
`MyList` is an algebraic data type, `map` is transforming this algebraic data type, and therefore structural recursion is applicable.
We now apply the structural recursion strategy, giving us

Expand Down Expand Up @@ -120,7 +120,7 @@ enum MyList[A] {
}
```

I've left the `???` to indicate that we haven't finished with this case.
I left the `???` to indicate that we haven't finished with that case.

Now we can move on to the problem specific parts.
Here we have three strategies to help us:
Expand All @@ -131,7 +131,7 @@ Here we have three strategies to help us:

Let's briefly discuss each and then see how they apply to our example.

The first strategy is relatively simple: when we consider the problem specific code on the right hand side of a pattern matching `case`, we can ignore the code in any other pattern matches. So, for example, when considering the case for `Empty` above, we don't need to worry about the case for `Pair`, and vice versa.
The first strategy is relatively simple: when we consider the problem specific code on the right hand side of a pattern matching `case`, we can ignore the code in any other pattern match cases. So, for example, when considering the case for `Empty` above, we don't need to worry about the case for `Pair`, and vice versa.

The next strategy is a little bit more complicated, and has to do with recursion. Remember that the structural recursion strategy tells us where to place any recursive calls. This means we don't have to think about the recursion. The result is guaranteed to be correct so long as we get the non-recursive parts correct.

Expand All @@ -156,7 +156,7 @@ enum MyList[A] {
}
```

Our first strategy is to consider the cases independently. Let's start with the `Empty` case. There is no recursive call here, so reasoning using structural recursion doesn't come into play here. Let's instead use the types. There is no input here other than the `Empty` case we have already matched, so we cannot use the input types to further restrict the code. However can use the output types. We're trying to create a `MyList[B]`. There are only two ways to create a `MyList[B]`: an `Empty` or a `Pair`. To create a `Pair` we need a `head` of type `B`, which we don't have. So we can only use `Empty`. *This is the only possible code we can write*. The types are sufficiently restrictive that we cannot write incorrect code for this case.
Our first strategy is to consider the cases independently. Let's start with the `Empty` case. There is no recursive call here, so reasoning using structural recursion doesn't come into play. Let's instead use the types. There is no input here other than the `Empty` case we have already matched, so we cannot use the input types to further restrict the code. However can use the output types. We're trying to create a `MyList[B]`. There are only two ways to create a `MyList[B]`: an `Empty` or a `Pair`. To create a `Pair` we need a `head` of type `B`, which we don't have. So we can only use `Empty`. *This is the only possible code we can write*. The types are sufficiently restrictive that we cannot write incorrect code for this case.

```scala
enum MyList[A] {
Expand Down Expand Up @@ -213,7 +213,7 @@ If you've followed this example you've hopefully see how we can use the three st
Remember that algebraic data types are a closed world: they cannot be extended once defined.
The Scala compiler can use this to check that we handle all possible cases in a pattern match,
so long as we write the pattern match in a way the compiler can work with.
This is known as **exhaustivity checking**.
This is known as exhaustivity checking.

Here's a simple example.
We start by defining a straight-forward algebraic data type.
Expand Down

0 comments on commit 711984f

Please sign in to comment.