Option as an effect

The first consequence of the aforementioned approach is that it is possible to constrain the possible values of the Option (and convert it to None if the conditions don't hold) without inspecting its contents. Here is an example of filtering options further containing a number bigger or less than 10, respectively:

val moreThen10: Option[Int] = opt.filter(_ > 10)
val lessOrEqual10: Option[Int] = opt.filterNot(_ > 10)

It is also possible to use a partial function as a filter. This allows you to filter and transform the value at the same time. For example, you can filter numbers bigger than 10 and convert them into a String:

val moreThen20: Option[String] = opt.collect {
case i if i > 20 => s"More then 20: $i"
}

Functionally, the collect method can be seen as a combination of filter and map, where the latter can be used separately to transform the contents of a non-empty option. For instance, let's imagine a chain of calls we'd need to make in order to catch a fish:

val buyBait: String => Bait = ???
val castLine: Bait => Line = ???
val hookFish: Line => Fish = ???

def goFishing(bestBaitForFish: Option[String]): Option[Fish] =
bestBaitForFish.map(buyBait).map(castLine).map(hookFish)

Here, we're buying some bait, casting the line, and hooking the fish at the appropriate moment. The argument for our implementation is optional because we might not know what the best bite for a fish would be.

There is an issue with this implementation, though. We're ignoring the fact that our functions will have no results for the given parameters. The fishing store might be closed, the cast might break, and the fish can slip out. It turns out that we violate our own rules about expressing effects with types that we defined a couple of pages ago!

Let's fix that by making our functions return Option. We'll start with hookFish:

val hookFish: Line => Option[Fish]

def goFishingOld(bestBaitForFish: Option[String]): Option[Option[Fish]] =
bestBaitForFish.map(buyBait).map(castLine).map(hookFish)

But now our function returns a nested Option, which is hard to work with. We can address this by flattening the result using the corresponding method:

def goFishingOld(bestBaitForFish: Option[String]): Option[Fish] =
bestBaitForFish.map(buyBait).map(castLine).map(hookFish).flatten

Now, we can also make the castLine return Option:

val castLine: Bait => Option[Line]
val hookFish: Line => Option[Fish]

def goFishingOld(bestBaitForFish: Option[String]): Option[Fish] =
bestBaitForFish.map(buyBait).map(castLine).map(hookFish).flatten

Unfortunately, this implementation ceases to compile:

error: type mismatch;
found : FishingOptionExample.this.Line => Option[FishingOptionExample.this.Fish]
required: Option[UserExample.this.Line] => ?

To deal with chained, non-empty options, there is a flatMap method, which accepts a function returning an Option and flattens the result before returning it. With flatMap, we can implement our chain of calls without the need to call flatten at the end:

val buyBait: String => Option[Bait]
val makeBait: String => Option[Bait]
val castLine: Bait => Option[Line]
val hookFish: Line => Option[Fish]

def goFishingOld(bestBaitForFish: Option[String]): Option[Fish] bestBaitForFish.flatMap(buyBait).flatMap(castLine).flatMap(hookFish)

Having map and flatMap also allows us to use Option in for comprehensions. For instance, we can rewrite the preceding example like so:

def goFishing(bestBaitForFish: Option[String]): Option[Fish] =
for {
baitName <- bestBaitForFish
bait <- buyBait(baitName).orElse(makeBait(baitName))
line <- castLine(bait)
fish <- hookFish(line)
} yield fish

Here, we also added a fallback case for the situation of the fishing shop being closed, and when you need to make the bait by hand. This demonstrates that empty options can also be chained. The orElse method resolves a series of options until the first one that's defined is found or returns the last Option in the chain, regardless of its contents:

val opt5 = opt0 orElse opt2 orElse opt3 orElse opt4

There is a possibility to map over the Option and provide a default value for the empty case. This is done with the fold method, which accepts the default value as a first argument list and a mapping function as a second one:

opt.fold("Value for an empty case")((i: Int) => s"The value is $i")

The last pair of methods available for an Option are toRight and toLeft. They return instances of the next effect we want to take a look at, Either. toRight returns Left, which contains its argument for None, or Right, containing the value of Some:

opt.toRight("If opt is empty, I'll be Left[String]")

toLeft does the same but returns on different sides of Either, respectively:

scala> val opt = Option.empty[String]
opt: Option[String] = None

scala> opt.toLeft("Nonempty opt will be Left, empty - Right[String]")
res8: Either[String,String] = Right(Nonempty opt will be Left, empty - Right[String])

But what are these Left and Right options we are talking about?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.71.94