1- # import " ../stdlib.typ" : info , warning , solution
1+ # import " ../stdlib.typ" : info , warning , solution , exercise
22== Either
33
44
55Let's look at another useful monad:
66the `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
2830for {
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
66117In 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
75128val a = 3.asRight[String]
76129val b = 4.asRight[String]
82135```
83136
84137These "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`
87140instead of `Left` and `Right` .
88141This helps avoid type inference problems
89142caused by over-narrowing,
@@ -128,7 +181,7 @@ countPositive(List(1, 2, 3))
128181countPositive(List(1, -2, 3))
129182```
130183
131- `cats. syntax.either` adds
184+ The Cats syntax also adds
132185some useful extension methods
133186to the `Either` companion object.
134187The `catchOnly` and `catchNonFatal` methods
@@ -148,25 +201,12 @@ Either.fromTry(scala.util.Try("foo".toInt))
148201Either.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
155208some 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-
170210The `ensure` method allows us
171211to check whether the right-hand value
172212satisfies a predicate:
@@ -206,89 +246,10 @@ The `swap` method lets us exchange left for right:
206246Finally, 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
293254Is the error handling strategy in the previous examples
294255well suited for all purposes?
0 commit comments