Skip to content

Instantly share code, notes, and snippets.

@fmonniot
Created May 28, 2021 22:04
Show Gist options
  • Save fmonniot/ede64214b941a72295b008f2950dc5a4 to your computer and use it in GitHub Desktop.
Save fmonniot/ede64214b941a72295b008f2950dc5a4 to your computer and use it in GitHub Desktop.
Introduction to type classes and some of their usage pattern
// 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