Skip to content

Commit eb5d5f8

Browse files
committed
Update monads either and id
1 parent b8d288c commit eb5d5f8

File tree

4 files changed

+139
-163
lines changed

4 files changed

+139
-163
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#import "../../stdlib.typ": info, warning, solution, chapter
2+
#chapter[Error Handling] <sec:error-handling>

src/pages/fps.typ

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@
189189
#include "parts/part4.typ"
190190
#include "usability/index.typ"
191191
#include "case-studies/testing/index.typ"
192+
#include "case-studies/error-handling/index.typ"
192193
#include "case-studies/map-reduce/index.typ"
193194
#include "case-studies/validation/index.typ"
194195
#include "case-studies/validation/sketch.typ"

src/pages/monads/either.typ

Lines changed: 104 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,129 @@
1-
#import "../stdlib.typ": info, warning, solution
1+
#import "../stdlib.typ": info, warning, solution, exercise
22
== Either
33

44

55
Let's look at another useful monad:
66
the `Either` type from the Scala standard library.
7-
In Scala 2.11 and earlier,
8-
many people didn't consider `Either` a monad
9-
because it didn't have `map` and `flatMap` methods.
10-
In Scala 2.12, however, `Either` became _right biased_.
11-
12-
=== Left and Right Bias
7+
`Either` has two cases, `Left` and `Right`.
8+
By convention `Right` represents a success case,
9+
and `Left` a failure.
10+
When we call `flatMap` on `Either`, computation continues if we have a `Right` case.
1311

12+
```scala mdoc
13+
Right(10).flatMap(a => Right(a + 32))
14+
```
1415

15-
In Scala 2.11, `Either` had no default
16-
`map` or `flatMap` method.
17-
This made the Scala 2.11 version of `Either`
18-
inconvenient to use in for comprehensions.
19-
We had to insert calls to `.right`
20-
in every generator clause:
16+
A `Left`, however, stops the computation.
2117

22-
```scala mdoc:silent:reset-object
23-
val either1: Either[String, Int] = Right(10)
24-
val either2: Either[String, Int] = Right(32)
18+
```scala mdoc
19+
Right(10).flatMap(a => Left("Oh no!"))
2520
```
2621

27-
```scala
22+
AS these examples suggest,
23+
`Either` is typically used to implement fail-fast error handling.
24+
We sequence computations using `flatMap` as usual.
25+
If one computation fails,
26+
the remaining computations are not run.
27+
Here's an example where we fail if we attempt to divide by zero.
28+
29+
```scala mdoc
2830
for {
29-
a <- either1.right
30-
b <- either2.right
31-
} yield a + b
31+
a <- Right(1)
32+
b <- Right(0)
33+
c <- if(b == 0) Left("DIV0")
34+
else Right(a / b)
35+
} yield c * 100
3236
```
3337

34-
In Scala 2.12, `Either` was redesigned.
35-
The modern `Either` makes the decision
36-
that the right side represents the success case
37-
and thus supports `map` and `flatMap` directly.
38-
This makes for comprehensions much more pleasant:
38+
We can see `Either` as similar to `Option`,
39+
but it allows us to record some information in the case of failure,
40+
whereas `Option` represents failure by `None`.
41+
In the examples above we used strings to hold information about the cause of failure,
42+
but we can use any type we like.
43+
For example, we could use `Throwable` instead:
3944

40-
```scala mdoc
41-
for {
42-
a <- either1
43-
b <- either2
44-
} yield a + b
45+
```scala mdoc:silent
46+
type Result[A] = Either[Throwable, A]
47+
```
48+
49+
This gives us similar semantics to `scala.util.Try`.
50+
The problem, however, is that `Throwable`
51+
is an extremely broad type.
52+
We have (almost) no idea about what type of error occurred.
53+
54+
Another approach is to define an algebraic data type
55+
to represent errors that may occur in our program:
56+
57+
```scala mdoc:silent
58+
enum LoginError {
59+
case UserNotFound(username: String)
60+
case PasswordIncorrect(username: String)
61+
case UnexpectedError
62+
}
4563
```
4664

47-
Cats back-ports this behaviour to Scala 2.11
48-
via the `cats.syntax.either` import,
49-
allowing us to use right-biased `Either`
50-
in all supported versions of Scala.
51-
In Scala 2.12+ we can either omit this import
52-
or leave it in place without breaking anything:
65+
We could use the `LoginError` type along with `Either` as shown below.
5366

5467
```scala mdoc:silent
55-
import cats.syntax.either.* // for map and flatMap
68+
case class User(username: String, password: String)
5669
57-
for {
58-
a <- either1
59-
b <- either2
60-
} yield a + b
70+
type LoginResult = Either[LoginError, User]
6171
```
6272

63-
=== Creating Instances
73+
This approach solves the problems we saw with `Throwable`.
74+
It gives us a fixed set of expected error types
75+
and a catch-all for anything else that we didn't expect.
76+
We also get the safety of exhaustivity checking
77+
on any pattern matching we do:
78+
79+
```scala mdoc:silent
80+
import LoginError.*
81+
82+
// Choose error-handling behaviour based on type:
83+
def handleError(error: LoginError): Unit =
84+
error match {
85+
case UserNotFound(u) =>
86+
println(s"User not found: $u")
87+
88+
case PasswordIncorrect(u) =>
89+
println(s"Password incorrect: $u")
6490
91+
case UnexpectedError =>
92+
println(s"Unexpected error")
93+
}
94+
```
95+
96+
Here's an example of use.
97+
98+
```scala mdoc
99+
val result1: LoginResult = Right(User("dave", "passw0rd"))
100+
val result2: LoginResult = Left(UserNotFound("dave"))
101+
102+
result1.fold(handleError, println)
103+
result2.fold(handleError, println)
104+
```
105+
106+
We have much more to say about error handling in @sec:error-handling.
107+
108+
109+
=== Cats Utilities
110+
111+
Cats provides several utilities for working with `Either`.
112+
Here we go over the most useful of them.
113+
114+
115+
==== Creating Instances
65116

66117
In addition to creating instances of `Left` and `Right` directly,
67-
we can also import the `asLeft` and `asRight` extension methods
68-
from [`cats.syntax.either`][cats.syntax.either]:
118+
we can also use the `asLeft` and `asRight` extension methods
119+
from Cats. For these methods we need to import the Cats syntax:
69120

70121
```scala mdoc:silent
71-
import cats.syntax.either.* // for asRight
122+
import cats.syntax.all.*
72123
```
73124

125+
Now we can construct instances using the extensions.
126+
74127
```scala mdoc
75128
val a = 3.asRight[String]
76129
val b = 4.asRight[String]
@@ -82,8 +135,8 @@ for {
82135
```
83136

84137
These "smart constructors" have
85-
advantages over `Left.apply` and `Right.apply`
86-
because they return results of type `Either`
138+
advantages over `Left.apply` and `Right.apply`.
139+
They return results of type `Either`
87140
instead of `Left` and `Right`.
88141
This helps avoid type inference problems
89142
caused by over-narrowing,
@@ -128,7 +181,7 @@ countPositive(List(1, 2, 3))
128181
countPositive(List(1, -2, 3))
129182
```
130183

131-
`cats.syntax.either` adds
184+
The Cats syntax also adds
132185
some useful extension methods
133186
to the `Either` companion object.
134187
The `catchOnly` and `catchNonFatal` methods
@@ -148,25 +201,12 @@ Either.fromTry(scala.util.Try("foo".toInt))
148201
Either.fromOption[String, Int](None, "Badness")
149202
```
150203

151-
=== Transforming Eithers
152204

205+
==== Transforming Eithers
153206

154-
`cats.syntax.either` also adds
207+
Cats syntax also adds
155208
some useful methods for instances of `Either`.
156209

157-
Users of Scala 2.11 or 2.12
158-
can use `orElse` and `getOrElse` to extract
159-
values from the right side or return a default:
160-
161-
```scala mdoc:silent
162-
import cats.syntax.either.*
163-
```
164-
165-
```scala mdoc
166-
"Error".asLeft[Int].getOrElse(0)
167-
"Error".asLeft[Int].orElse(2.asRight[String])
168-
```
169-
170210
The `ensure` method allows us
171211
to check whether the right-hand value
172212
satisfies a predicate:
@@ -206,89 +246,10 @@ The `swap` method lets us exchange left for right:
206246
Finally, Cats adds a host of conversion methods:
207247
`toOption`, `toList`, `toTry`, `toValidated`, and so on.
208248

209-
=== Error Handling
210-
211-
212-
`Either` is typically used to implement fail-fast error handling.
213-
We sequence computations using `flatMap` as usual.
214-
If one computation fails,
215-
the remaining computations are not run:
216-
217-
```scala mdoc
218-
for {
219-
a <- 1.asRight[String]
220-
b <- 0.asRight[String]
221-
c <- if(b == 0) "DIV0".asLeft[Int]
222-
else (a / b).asRight[String]
223-
} yield c * 100
224-
```
225-
226-
When using `Either` for error handling,
227-
we need to determine
228-
what type we want to use to represent errors.
229-
We could use `Throwable` for this:
230-
231-
```scala mdoc:silent
232-
type Result[A] = Either[Throwable, A]
233-
```
234-
235-
This gives us similar semantics to `scala.util.Try`.
236-
The problem, however, is that `Throwable`
237-
is an extremely broad type.
238-
We have (almost) no idea about what type of error occurred.
239-
240-
Another approach is to define an algebraic data type
241-
to represent errors that may occur in our program:
242-
243-
```scala mdoc:silent
244-
enum LoginError {
245-
case UserNotFound(username: String)
246249

247-
case PasswordIncorrect(username: String)
248-
249-
case UnexpectedError
250-
}
251-
```
252-
253-
```scala mdoc:silent
254-
case class User(username: String, password: String)
255-
256-
type LoginResult = Either[LoginError, User]
257-
```
258-
259-
This approach solves the problems we saw with `Throwable`.
260-
It gives us a fixed set of expected error types
261-
and a catch-all for anything else that we didn't expect.
262-
We also get the safety of exhaustivity checking
263-
on any pattern matching we do:
264-
265-
```scala mdoc:silent
266-
import LoginError.*
267-
268-
// Choose error-handling behaviour based on type:
269-
def handleError(error: LoginError): Unit =
270-
error match {
271-
case UserNotFound(u) =>
272-
println(s"User not found: $u")
273-
274-
case PasswordIncorrect(u) =>
275-
println(s"Password incorrect: $u")
276-
277-
case UnexpectedError =>
278-
println(s"Unexpected error")
279-
}
280-
```
281-
282-
```scala mdoc
283-
val result1: LoginResult = User("dave", "passw0rd").asRight
284-
val result2: LoginResult = UserNotFound("dave").asLeft
285-
286-
result1.fold(handleError, println)
287-
result2.fold(handleError, println)
288-
```
289250

290-
=== Exercise: What is Best?
291251

252+
#exercise([What is Best?])
292253

293254
Is the error handling strategy in the previous examples
294255
well suited for all purposes?

0 commit comments

Comments
 (0)