Created
May 28, 2021 22:04
-
-
Save fmonniot/ede64214b941a72295b008f2950dc5a4 to your computer and use it in GitHub Desktop.
Introduction to type classes and some of their usage pattern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ignore this, boilerplate for later | |
import Instances._ | |
import scala.concurrent.Future | |
////////////////////////////////////////////////// | |
//// Part 0: Introduction to Cats //// | |
////////////////////////////////////////////////// | |
// May 28th, 2021 | |
/* | |
First SmartThings Scala Meetup, Welcome everyone ! | |
New format: | |
no slides, all code. | |
IntelliJ is the new PowerPoint | |
*/ | |
/* Some interesting links: | |
- https://typelevel.org/cats/resources_for_learners.html | |
- https://www.baeldung.com/scala/cats-intro | |
- https://www.scala-exercises.org/cats/monad | |
*/ | |
////////////////////////////////////////////////// | |
//// Part 1: What & How type classes in Scala //// | |
////////////////////////////////////////////////// | |
/* | |
Q: What is a type class ? | |
A: A type class is a pattern in programming originating in Haskell. | |
It allows us to extend existing libraries with new functionality, | |
without using traditional inheritance, and without altering the original library source code. | |
S: Examples of functions we will see today: | |
Option.map, Result.map, Try.map, Map.map, Tuple.map | |
Q: How do we write type classes in Scala ? | |
*/ | |
// Example of a type class. It is an interface with at least one type parameter. | |
// Can also be though as a "capability of the type A" | |
trait Area[A] { | |
def area(a: A): Double | |
} | |
// A type class is implemented for a given type as an implicit value | |
case class Rectangle(width: Double, length: Double) | |
object Rectangle { | |
implicit val area: Area[Rectangle] = new Area[Rectangle] { | |
override def area(a: Rectangle): Double = a.width * a.length | |
} | |
} | |
// The type class can then be used without knowing about the implementation | |
object ShapeArea { | |
def areaOf[A](a: A)(implicit shape: Area[A]): Double = shape.area(a) | |
} | |
object part1 { | |
// Here, the compiler will fill in the implicit shape parameter for us | |
val rectangleArea: Double = ShapeArea.areaOf(Rectangle(1, 2)) | |
} | |
// | |
// Short detour: values, types and kinds | |
// | |
object valuesTypesKinds { | |
val value = 42 // `value` is a value | |
class Type // `Type` is a type | |
val t = new Type // `t` is a value of type `Type` | |
class Type2[T] // `Type2` is a type that take another type as a parameter | |
// T is a complete type (eg. Int) | |
class Type3[T[_]] // `Type3` is a type that take a type that take a type as a parameter | |
// T is a partial type: it needs another type to be complete (eg. Option) | |
new Type3[Type2] | |
// Kind is like types for values, but for types | |
// Type2 is noted: * | |
// Type3 is noted: * -> * | |
} | |
// Example of a type class which works with higher kinded type | |
object higherKindedTypeClasses { | |
trait Getter[F[_]] { | |
def get[A](fa: F[A]): A | |
} | |
object Getter { | |
def apply[F[_]](implicit getter: Getter[F]): Getter[F] = getter | |
// Please don't do that in production, it's not safe :) | |
implicit val option: Getter[Option] = new Getter[Option] { | |
override def get[A](fa: Option[A]): A = fa.get | |
} | |
} | |
val fortyTwo: Int = Getter[Option].get(Some(42)) | |
} | |
/////////////////////////////////////////////// | |
//// Part 2: some interesting type classes //// | |
/////////////////////////////////////////////// | |
// Within a context F, transform a type A into a type B | |
trait Functor[F[_]] { | |
def map[A, B](fa: F[A])(f: A => B): F[B] | |
} | |
object Functor { | |
def apply[A[_]](implicit f: Functor[A]): Functor[A] = f | |
} | |
/* Examples: | |
Some(42).map(i => i + 1) //=> Some(43) | |
Future(42).map(i => i + 1) //=> Future(43) | |
Try(42).map(i => i + 1) //=> Try(43) | |
*/ | |
// Apply a function in a context to a value in the same context | |
// Also lift a value into a context | |
trait Applicative[F[_]] extends Functor[F] { | |
// For completeness sake, but not used that much in practice | |
//def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] | |
def pure[A](x: A): F[A] | |
} | |
object Applicative { | |
def apply[A[_]](implicit a: Applicative[A]): Applicative[A] = a | |
} | |
/* | |
Examples | |
Applicative[Option].pure(42) //=> Some(42) | |
Applicative[Future].pure(42) //=> Future(42) | |
// IO, Future, Task | |
def doSomeWork[F[_]: Applicative] = Applicative[F].pure(()).map(() => 42) | |
def doSomeWork[F[_]: Monad] = Monad[F].pure(()) | |
def doSomeWork[F[_]: Sync] = Sync[F].pure(()) | |
Did you notice? there is a hierarchy | |
So `Applicative[Option].map` is defined ! | |
*/ | |
// Run operations sequentially | |
trait Monad[F[_]] extends Applicative[F] { | |
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] | |
//def flatten[A](fa: F[F[A]]): F[A] | |
override def map[A, B](fa: F[A])(f: A => B): F[B] = | |
flatMap(fa)(a => pure(f(a))) | |
} | |
object Monad { | |
def apply[A[_]](implicit monad: Monad[A]): Monad[A] = monad | |
} | |
object monadExample { | |
// some business logic in practice | |
type PrevResult = Int | |
type NextResult = Int | |
val previousApiCall: Future[PrevResult] = ??? | |
def nextApiCall(prev: PrevResult): Future[NextResult] = ??? | |
val result: Future[NextResult] = | |
Monad[Future].flatMap(previousApiCall)(r => nextApiCall(r)) | |
} | |
object forcomp { | |
val _ = Option(42).flatMap { r1 => | |
Option(2).flatMap { r2 => | |
val e = "" | |
Option(3).map { r3 => | |
r1 + r2 + r3 | |
} | |
} | |
} | |
val _ = for { | |
r1 <- Option(42) | |
r2 <- Option(2) | |
e = "" | |
r3 <- Option(3) | |
} yield r1 + r2 + r3 | |
} | |
/////////////////////////////////// | |
//// Part 3: Monad Transformer //// | |
/////////////////////////////////// | |
/* | |
We often works with types like F[Either[Error, Success]] | |
*/ | |
object part3a { | |
def businessLogic[F[_]](anApiCall: F[Either[Error, Int]])(implicit F: Monad[F]): F[Either[Error, Int]] = { | |
def validateNumber(i: Int) = F.pure(if (i > 42) Right(i) else Left(new Error("invalid number"))) | |
// Note how we have to map on F and then map/flatMap on the inner Either | |
// Imagine we have to do that on every operation. It's becoming very annoying. | |
F.flatMap(anApiCall) { either => | |
// We can't use flatMap, because of the inner F | |
val _: Either[Error, F[Either[Error, Int]]] = | |
Monad[Either[Error, *]].map(either) { n => | |
validateNumber(n) // Return F and not Either | |
} | |
// We can use pattern matching, but it's a bit annoying to forward the error case | |
either match { | |
case Left(error) => F.pure(Left(error)) | |
case Right(value) => validateNumber(value) | |
} | |
} | |
} | |
} | |
// Introducing EitherT (Either Transformer) | |
// It is a simple wrapper but offers type classes implementation which abstract the inner content | |
case class EitherT[F[_], A, B](value: F[Either[A, B]]) | |
// previous example rewritten with EitherT | |
object part3b { | |
def businessLogic[F[_]](anApiCall: F[Either[Error, Int]])(implicit F: Monad[F]): F[Either[Error, Int]] = { | |
def validateNumber(i: Int) = F.pure(if (i > 42) Right(i) else Left(new Error("invalid number"))) | |
val FE = Monad[EitherT[F, Error, *]] // Either[A, B] => A == Error, B == generic | |
// Note how we have to map on F and then map/flatMap on the inner Either | |
// Imagine we have to do that on every operation. It's becoming very annoying. | |
FE.flatMap(EitherT(anApiCall)) { n: Int => | |
EitherT(validateNumber(n)) | |
}.value | |
/* | |
for { | |
n <- EitherT(anApiCall) | |
r <- EitherT(validateNumber(n)) | |
} yield r | |
*/ | |
} | |
} | |
//////////////////////////////// | |
//// Bonus Content: Kleisli //// | |
//////////////////////////////// | |
// Represents a function `A => F[B]`. | |
case class Kleisli[F[_], -A, B](run: A => F[B]) | |
// Why would we do that ? | |
// Because now we have access to all the type classes and we can compose function very easily | |
object kleisliExample { | |
// We will be using HTTP as an example, as its request-response model is a | |
// good fit for Kleisli. | |
import cats.data.Kleisli | |
import cats.effect._ | |
import cats.syntax.all._ | |
import org.http4s._ | |
import org.http4s.dsl.io._ | |
// HttpRoutes is more or less Kleisli[F, Request[F], Response[F]] | |
val service: HttpRoutes[IO] = HttpRoutes.of[IO] { | |
case GET -> Root / "bad" => | |
BadRequest() | |
case _ => | |
Ok() | |
} | |
def myMiddle(service: HttpRoutes[IO], header: Header): HttpRoutes[IO] = Kleisli { (req: Request[IO]) => | |
service(req).map { | |
case Status.Successful(resp) => | |
resp.putHeaders(header) | |
case resp => | |
resp | |
} | |
} | |
val wrappedService: HttpRoutes[IO] = myMiddle(service, Header("SomeKey", "SomeValue")) | |
val apiService: HttpRoutes[IO] = HttpRoutes.of[IO] { | |
case GET -> Root / "api" => | |
Ok() | |
} | |
// Composition is made easy | |
val aggregate: HttpRoutes[IO] = apiService <+> wrappedService | |
} | |
//////////////////////////////////////// | |
//// Bonus Content: implicit scopes //// | |
//////////////////////////////////////// | |
// Where does the implicit parameters comes from ? | |
// How does the compiler know where to find them ? | |
// 1. Lexical Scope | |
object implicitScopes11 { | |
implicit val n: Int = 5 | |
implicitly[Int] // takes y from the current scope | |
} | |
object implicitScopes12 { | |
import implicitScopes11._ // bring n and scale into scope | |
implicitly[Int] // takes y from the current scope, through the import | |
} | |
// 2. Implicit scope: Companion object | |
object implicitScopes21 { | |
trait T[A] | |
object T { | |
implicit val t: T[String] = new T[String] {} | |
implicit val t2: T[Int] = new T[Int] {} | |
} | |
implicitly[T[String]] // the compiler will look for an implicit parameter in the companion object | |
} | |
object implicitScopes22 { | |
//import implicitScopes21.T | |
//implicitly[implicitScopes21.T] // And that works everywhere the type is accessible | |
} | |
// Note: in case of class hierarchy, the companion objects of super classes are also part of the search path | |
// 3. Implicit scope: type arguments | |
object implicitScopes31 { | |
// library | |
trait T[A] | |
object T { | |
implicit val t: T[String] = new T[String] {} | |
implicit val t2: T[Int] = new T[Int] {} | |
} | |
class A(val n: Int) | |
object A { | |
implicit val ord: Ordering[A] = Ordering.by(_.n) | |
implicit val t: T[A] = new T[A] {} | |
} | |
implicitly[Ordering[A]] | |
} | |
object implicitScopes32 { | |
import implicitScopes31.A | |
implicitly[Ordering[A]] // Found the implicit value in the companion object of A | |
} | |
//////////////////////////////////////////////////// | |
//// Bonus Content: type classes implementation //// | |
//////////////////////////////////////////////////// | |
object Instances { | |
implicit def eitherMonad[Err]: Monad[Either[Err, *]] = | |
new Monad[Either[Err, *]] { | |
def flatMap[A, B](fa: Either[Err, A])(f: A => Either[Err, B]): Either[Err, B] = | |
fa.flatMap(f) | |
def pure[A](x: A): Either[Err, A] = Right(x) | |
} | |
implicit def futureMonad: Monad[Future] = new Monad[Future] { | |
import scala.concurrent.ExecutionContext.Implicits.global | |
override def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = | |
fa.flatMap(f) | |
override def pure[A](x: A): Future[A] = Future.successful(x) | |
} | |
implicit def kleisliMonad[F[_], C](implicit F: Monad[F]): Monad[Kleisli[F, C, *]] = new Monad[Kleisli[F, C, *]] { | |
override def flatMap[A, B](k: Kleisli[F, C, A])(f: A => Kleisli[F, C, B]): Kleisli[F, C, B] = | |
Kleisli { c: C => | |
val fa: F[A] = k.run(c) | |
F.flatMap(fa) { a => | |
f(a).run(c) | |
} | |
} | |
override def pure[A](x: A): Kleisli[F, C, A] = | |
Kleisli(_ => F.pure(x)) | |
} | |
implicit def eitherTMonad[F[_], Err](implicit F: Monad[F]): Monad[EitherT[F, Err, *]] = new Monad[EitherT[F, Err, *]] { | |
override def flatMap[A, B](fa: EitherT[F, Err, A])(f: A => EitherT[F, Err, B]): EitherT[F, Err, B] = | |
EitherT(F.flatMap(fa.value) { | |
case Left(err) => F.pure(Left(err)) | |
case Right(b) => f(b).value | |
}) | |
override def pure[A](x: A): EitherT[F, Err, A] = EitherT(F.pure(x)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment