Functor

In the previous chapter, we discussed the situation in which we wanted to combine elements inside of a container. We found out that abstractions such as Reducible and Foldable can help with that by taking a function of two arguments and bringing it into the container so that it can be applied on pairs of elements inside of it. As an example, we showed you how this approach makes it possible to implement different survival strategies for a bucket of fish.

What we haven't covered is a case where we don't want to combine elements in the container but do something with all of them, a single element at a time. This is the bread and butter of functional programming—applying pure functions to arguments and getting the results back, then repeating this process with the result. Usually, the functions applied to the argument in succession can be combined into a single function, which in a sense is fusing all of the intermediate steps into one step.

Back to our fish example. Let's imagine that we have a fish and that we'd like to eat it. We'd first check that the fish is healthy and still fresh, then we would cook it somehow, and finally, we'd consume it. We might represent this sequence with the following model, extending the original Fish definition from the previous chapter:

final case class Fish(volume: Int, weight: Int, teeth: Int, poisonousness: Int)

sealed trait Eatable

final case class FreshFish(fish: Fish)
final case class FriedFish(weight: Int) extends Eatable
final case class CookedFish(goodTaste: Boolean) extends Eatable
final case class Sushi(freshness: Int) extends Eatable

And our possible actions would naturally be represented as functions:

import ch08.Model._
import ch08.ModelCheck._
val
check: Fish => FreshFish = f => FreshFish(f)
val prepare: FreshFish => FriedFish = f => FriedFish(f.fish.weight)
val eat: Eatable => Unit = _ => println("Yum yum...")

Then, we might want to combine our actions so that they represent the whole process from fresh to eaten fish:

def prepareAndEat: Fish => Unit = check andThen prepare andThen eat

Now, we can act on the fish as desired by applying the combined function to the fish:

val fish: Fish = fishGen.sample.get
val freshFish = check(fish)

In this example, we're using the Gen[Fish] function we defined in the previous chapter. Please consult the GitHub repository if you need to refresh your understanding on how this was done.

So far so good—we're satisfied and happy. But the situation will change if we have a bucket of fish. Suddenly, all of the functions we've defined are useless because we don't know how to apply them to the fish inside of the bucket! What do we do now?

The requirement to work "inside" of the bucket might sound strange, but it is only because our example is disconnected from the implementation. In programming, most of the time, working with collections implies that we have the same collection (though with changed elements) after applying the operation. Moreover, if the structure of the collection is preserved, then the category theory we mentioned previously can provide some guarantees in regard to combining the operations as long as these obey a required set of laws. We've seen how this works with abstract algebraic structures, and the principle is the same for all abstractions derived from category theory. In practice, the requirement to preserve the structure of the collection means that the operation cannot change the type of the collection or the number of elements in it or throw an exception.

It turns out that there is an abstraction that can help us in this situation.

The Functor has a map method which takes a container and a function and applies this function to all of the elements in the container, and finally returning the container with the same structure but filled with new elements. This is how we can specify this in Scala:

import scala.language.higherKinds
trait
Functor[F[_]] {
def map[A,B](in: F[A])(f: A => B): F[B]
}

F[_] is a type constructor for the container. The map itself takes a container and a function to apply and returns a container with new elements. We could also define the map slightly differently, in a curried form:

def mapC[A,B](f: A => B): F[A] => F[B]

Here, mapC takes a function called A => B and returns a function called F[A] => F[B], which can then be applied to the container. 

As this is an abstract definition, we would naturally expect some laws to be defined and satisfied—exactly like in the previous chapter. For functors, there are two of them:

  • The identity law states that mapping over an  identity function should not change the original collection
  • The distributive law requires that successive mapping over two functions should always produce the same result as mapping over the combination of these functions

We will capture these requirements as properties in the same that way we did in the previous chapter.

First, let's take a look at the identity law:

def id[A, F[_]](implicit F: Functor[F], arbFA: Arbitrary[F[A]]): Prop =
forAll { as: F[A] => F.map(as)(identity) == as }

In this property, we're using the identity function from Chapter 3Deep Dive into Functions, which just returns its argument. 

The associativity law is a bit more involved because we need to test it with random functions. This requires that a lot of implicits are available:

import org.scalacheck._
import org.scalacheck.Prop._

def associativity[A, B, C, F[_]](implicit F: Functor[F],
arbFA: Arbitrary[F[A]],
arbB: Arbitrary[B],
arbC: Arbitrary[C],
cogenA: Cogen[A],
cogenB: Cogen[B]): Prop = {
forAll((as: F[A], f: A => B, g: B => C) => {
F.map(F.map(as)(f))(g) == F.map(as)(f andThen g)
})
}

Here, we're creating the arbitrary functions f: A => B and g: B => C  and checking that the combined function has the same effect as applying both functions in succession.

Now, we need some functors to apply our checks. We can implement a Functor[Option] by delegating to the map function defined on Option:

implicit val optionFunctor: Functor[Option] = new Functor[Option] {
override def map[A, B](in: Option[A])(f: A => B): Option[B] = in.map(f)
def mapC[A, B](f: A => B): Option[A] => Option[B] = (_: Option[A]).map(f)
}

The instance is defined as implicit, the same way as in the previous chapter, so that represents a type class.

Does this implementation obeys the necessary laws? Let's see. The properties in this chapter are defined in the test scope and can be run in SBT using the test command. They cannot be pasted into the REPL standalone, but only as a part of the Properties definition:

property("Functor[Option] and Int => String, String => Long") = {
import Functor.optionFunctor
functor[Int, String, Long, Option]
}
+ Functor.Functor[Option] and Int => String, String => Long: OK, passed 100 tests.

property("Functor[Option] and String => Int, Int => Boolean") = {
import Functor.optionFunctor
functor[String, Int, Boolean, Option]
}
+ Functor.Functor[Option] and String => Int, Int => Boolean: OK, passed 100 tests.

We can see that we need to specify types of the functor and functions to check the laws that make it impossiblein our caseto formulate the functor properties in general. The functional programming library, cats, solves this problem by also defining type classes for the types of arguments. We'll stick to the explicit definition—this is sufficient for our learning purposes.

We can also implement functors for the other effects we saw in Chapter 6Exploring Built-In Effects in the same way we did for Option. The functor for Try is identical with respect to the type of effect. We'll leave this implementation as an exercise for the reader.

The case of Either is a bit more complicated, because we need to convert the two type arguments it takes to one type argument that's expected by a Functor type constructor. We do this by fixing a type of the left side to L and using the type lambda in the definition of the functor:

implicit def eitherFunctor[L] = new Functor[({ type T[A] = Either[L, A] })#T] {
override def map[A, B](in: Either[L, A])(f: A => B): Either[L, B] = in.map(f)
def mapC[A, B](f: A => B): Either[L, A] => Either[L, B] = (_: Either[L, A]).map(f)
}

Interestingly, the implementation itself is the same again. It turns out that this is the abstraction we were looking for at the end of Chapter 6, Exploring Built-In Effects. All of the standard effects we discussed in Chapter 6Exploring Built-In Effects are functors! The visible difference in the definition of the map method comes from the fact that, for the standard effects, it is defined using object-oriented polymorphism, and in our functor code, we're doing this by using ad-hoc polymorphism with type classes.

Let's get back to our fish. As we have a bucket of them, which is represented by the List type, we'll need a Functor[Bucket] as well:

implicit val bucketFunctor: Functor[List] = new Functor[List] {
override def map[A, B](in: List[A])(f: A => B): List[B] = in.map(f)
def mapC[A, B](f: A => B): List[A] => List[B] = (_: List[A]).map(f)
}

The definition is once again the same as before. However, we can perform actions on the fish in the bucket as desired now, reusing the bucketOfFishGen:

type Bucket[S] = List[S]
val bucketOfFishGen: Gen[List[Fish]] = Gen.listOf(fishGen)

val bucketOfFriedFish: Bucket[FriedFish] = ch08.Functor.bucketFunctor.map(bucketOfFishGen.sample.get)(check andThen prepare)

Here, we're using our freshly defined functor to check and prepare the fish inside of the bucket. The nice thing about our implementation is that the bucket can be any type that has a functor. To demonstrate this, we need a helper function that will allow us to pass a functor as a third parameter, along with the two we have in the definition of the Functor.map:

def mapFunc[A, B, F[_]](as: F[A])(f: A => B)(implicit functor: ch08.Functor[F]): F[B] = functor.map(as)(f)

This function takes an effect and a function and implicitly resolves the appropriate functor. The calling code does not make this distinction any more since we're mapping over three different types of effects in the same way by using different functions:

import ch08.Functor._
import ch08.ModelCheck._
{
type Bucket[S] = Option[S]
mapFunc(optionOfFishGen.sample.get)(check)
}
{
type Bucket[S] = Either[Exception, S]
mapFunc(eitherFishGen.sample.get)(check andThen prepare)
}
{
type Bucket[S] = List[S]
mapFunc(listOfFishGen.sample.get)(prepareAndEat)
}

Now, it is starting to look like a useful abstraction—well, as long as our desires are limited to the functions of one argument. We'll see why in the next section.

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

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