Share
Embed
Transcript
25 slides
-
Type Classes 101 // 1. Capture a concept trait Show[A]
{ def show(a: A): String } object Show { // 2. Define a method that accept, implicitly, an instance of the concept def show[A](a: A)(implicit ev: Show[A]): String = ev.show(a) } // 3. Implement some instances of the concept implicit val intShow = new Show[Int] { override def show(a: Int): String = a.toString } -
Type Classes 102 object Show { // Alternative syntax def
show[A: Show](a: A): String = implicitly[Show[A]].show(a) } -
Type Classes 103 object Show { // Commonly used pattern
def apply[A: Show] = implicitly[Show[A]] // All methods using Show can now use this syntax def show[A: Show](a: A): String = Show[A].show(a) } -
Higher-kinded types Kinds in Type Theory Symbol Kind Examples *
Simple type. AKA nullary type constructor or proper type. Int, String, Double, ... * -> * Unary type constructor. List, Option, Future, ... * -> * -> * Binary type constructor. Either, Function1, Map, ... (* -> *) -> * Higher-order type operator, higher-kinded type for friends. Foo[F[_]], Bar[G[_]], Functor[F[_]], Monad[M[_]], ... -
Example: Mappable Capturing the concept represented by the map method
(See: Option, …): trait Mappable[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] } object Mappable { def apply[F[_]: Mappable]: Mappable[F] = implicitly[Mappable[F]] def map[F[_]: Mappable, A, B](ma: F[A])(f: A => B): F[B] = Mappable[F].map(ma)(f) implicit val optionMapper = new Mappaple[Option] { override def map[A, B](oa: Option[A])(f: A => B): Option[B] = oa.map(f) } implicit val listMapper = new Mappaple[List] { override def map[A, B](la: List[A])(f: A => B): List[B] = la.map(f) } } -
Functor: Change the Mappable name... trait Functor[F[_]] { def map[A,
B](fa: F[A])(f: A => B): F[B] } object Functor { def apply[F[_]: Functor]: Functor[F] = implicitly[Functor[F]] def map[F[_]: Functor, A, B](ma: F[A])(f: A => B): F[B] = Functor[F].map(ma)(f) implicit val optionFunctor = new Functor[Option] { override def map[A, B](oa: Option[A])(f: A => B): Option[B] = oa.map(f) } implicit val listFunctor = new Functor[List] { override def map[A, B](la: List[A])(f: A => B): List[B] = la.map(f) } } -
...and add a couple of laws In order for something
to be a functor, it should satisfy a couple of laws: Given a functor fa: F[A] and the identity function defined as follows: def identity[A](a: A): A = a the following laws must hold (where the order of the args is swapped, the Haskell way): Identity Law: map(identity)(fa) = fa Composition Law: map(f compose g)(fa) = map(f)(map(g)(fa)) -
Identity Law map(identity)(fa) = fa => map(identity) = identity E.g.:
Option[A] import Functor._ val some: Option[Int] = Some(42) val none: Option[Int] = None assert(map(some)(identity) == some ) assert(map(none)(identity) == none ) -
Composition Law map(f compose g)(fa) = map(f)(map(g)(fa)) E.g.: Option[A] val
f: Int => Int = x => x + 42 val g: Int => Int = x => x * x assert(map(some)(f compose g) == map(map(some)(g))(f) ) assert(map(none)(f compose g) == map(map(none)(g))(f) ) Note: The compiler cannot enforce these laws. You need to ensure them yourself. -
Lessons Learned The Mappable concept was easier to get because
the name didn’t scare you. Rule I: Don’t let the buzzwords scare you Everything gets easier if you look closely at the types. Rule II: Always follow the types. -
Case Study A Functor lets us apply a function to
a value which is inside a context. Context examples: Option, List, Future. Now, what if the function you want to apply is within a context as well? E.g.: def interpret(str: String): Option[Int => Int] = str.toLowerCase match { case "incr" => Some(_ + 1) case "decr" => Some(_ - 1) case "square" => Some(x => x * x) case "halve" => Some(x => x / 2) case _ => None } val v: Option[Int] = Some(42) -
Enter The Applicative Functor Capturing the concept of a function
within a context, that is F[A => B]: trait Applicative[F[_]] extends Functor[F] { def pure[A](a: A): F[A] def ap[A, B](fa: F[A])(fab: F[A => B]): F[B] // from applicative you get a functor for free override def map[A, B](fa: F[A])(fab: A => B): F[B] = } object Applicative { def apply[F[_]: Applicative]: Applicative[F] = implicitly[Applicative[F]] def pure[F[_]: Applicative, A](a: A): F[A] = Applicative[F].pure(a) def ap[F[_]: Applicative, A, B](fa: F[A])(fab: F[A => B]): F[B] = Applicative[F].ap(fa)(fab) // ... Applicative instances } ap(fa)(pure(fab)) -
Applicative instances object Applicative { // ... implicit val optionApplicative
= new Applicative[Option] { override def pure[A](a: A): Option[A] = Option(a) override def ap[A, B](fa: Option[A])(fab: Option[A => B]): Option[B] = for { a <- fa f <- fab } yield f(a) } implicit val listApplicative = new Applicative[List] { override def pure[A](a: A): List[A] = List(a) override def ap[A, B](fa: List[A])(fab: List[A => B]): List[B] = for { a <- fa f <- fab } yield f(a) } } The Applicative Functor needs some laws satisfied as well that we won’t cover here. -
Case Study Solved import Applicative._ def interpret(str: String): Option[Int =>
Int] = str.toLowerCase match { case "incr" => Some(_ + 1) case "decr" => Some(_ - 1) case "square" => Some(x => x * x) case "halve" => Some(x => x / 2) case _ => None } val func: Option[Int => Int] = interpret("incr") val v: Option[Int] = Some(42) val result: Option[Int] = ap(v)(func) -
And now the dreadful Monad If C is a Category
a monad on C consists of an endofunctor T: C -> C together with two natural transformations: η: 1 C -> T (where 1 C denotes the identity functor on C) μ: T2 -> T (where T2 is the functor T T from C to C). These are required to fulfill the following conditions (sometimes called coherence conditions): μ Tμ = μ μ T (as natural transformations T3 -> T); μ T η = μ η T = 1 T (as natural transformations T -> T; here 1 T denotes the identity transformation from T to T). Just Kidding! Well, not really. That’s the Wikipedia definition -
Monad for developers trait Monad[M[_]] extends Applicative[M] { def unit[A](a:
A): M[A] def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] override def pure[A](a: A): M[A] = unit(a) override def ap[A, B](fa: M[A])(f: M[A => B]): M[B] = flatMap(fa)(a => map(f)(ff => ff(a))) } The primitives required by the Monad type class are just unit and flatMap or, equivalently, unit and join (along with map inherited from Functor): def join(mma: M[M[A]]): M[A] You can derive flatMap from unit, join and map. -
Monad continued... object Monad { def apply[M[_] : Monad]: Monad[M]
= implicitly[Monad[M]] def unit[M[_]: Monad, A](a: A): M[A] = Monad[M].unit(a) def flatMap[M[_]: Monad, A, B](ma: M[A])(f: A => M[B]): M[B] = Monad[M].flatMap(ma)(f) implicit val optionMonad = new Monad[Option] { override def unit[A](a: A): Option[A] = Option(a) override def flatMap[A, B](ma: Option[A])(f: A => Option[B]): Option[B] = ma.flatMap(f) } implicit val listMonad = new Monad[List] { override def unit[A](a: A): List[A] = List(a) override def flatMap[A, B](ma: List[A])(f: A => List[B]): List[B] = ma.flatMap(f) } } -
Monad Laws Do we need to write our own type
classes and implementations for concepts such as Functor, Applicative and Monad? In Scala pseudocode: Left Identity: unit(a).flatMap(f) == f(a) Right Identity: m.flatMap(unit) == m Associativity: (m.flatMap(f)).flatMap(g) == m.flatMap(a => f(a).flatMap(g)) Not really! -
Cats GitHub: https://github.com/typelevel/cats libraryDependencies += "org.typelevel" %% "cats" % catsVersion
Resources for Learners: http://typelevel.org/cats/ http://eed3si9n.com/herding-cats/ Cats is broken up into a number of sub-projects: • core - contains type class definitions (e.g. Functor, Applicative, Monad), essential datatypes, and type class instances for those datatypes and standard library types • laws - laws for the type classes, used to validate type class instances • tests - tests that check type class instances with laws from laws • docs - The source for this website -
Cats (0.4.0): Example import cats._ import cats.std.all._ val inc: Int
=> Int = _ + 1 val some: Option[Int] = Some(42) val none: Option[Int] = None val list: List[Int] = List(1, 2, 3) val emptyList: List[Int] = List() val optResult1 = Functor[Option].map(some)(inc) // Some(43) val optResult2 = Functor[Option].map(none)(inc) // None val listResult1 = Functor[List].map(list)(inc) // List(2, 3, 4) val listResult2 = Functor[List].map(emptyList)(inc) // List() Nice. But it would be more useful if we could abstract over Functor, wouldn’t it? -
Cats (0.4.0): Abstracting over Functor val inc: Int => Int
= _ + 1 val some: Option[Int] = Some(42) val none: Option[Int] = None val list: List[Int] = List(1, 2, 3) val emptyList: List[Int] = List() def map[F[_]: Functor, A, B](fa: F[A])(f: A => B): F[B] = Functor[F].map(fa)(f) val optResult3 = map(some)(inc) // Some(43) val listResult3 = map(list)(inc) // List(2, 3, 4) Great. Now what if we want to compose two functors? No problem man! -
Cats (0.4.0): Functors compose! val inc: Int => Int =
_ + 1 val listOfOpts = List(Some(1), None, Some(3)) // We want to map inc to listOfOpts val listOptFunctor = Functor[List] compose Functor[Option] val listOptResult = listOptFunctor.map(listOfOpts)(inc) // List(Some(2), None, Some(4)) Applicatives compose too. Monads do not, mechanically, compose. Some of them do and their composition is generally achieved through Monad transformers. -
Shameless Plug I dealt with these and other subjects in
this book: Professional Scala - Wrox URL: http://eu.wiley.com/WileyCDA/WileyTitle/productCd-1119267226.html