Last active
April 20, 2022 23:29
-
-
Save sagoez/e7090029a8c27e61c821f3efad3ff65b to your computer and use it in GitHub Desktop.
This file contains hidden or 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
sealed trait MyList[A] | |
object MyList { | |
def empty[A]: MyList[A] = Empty() | |
/// A* means zero or more (variadic) | |
def apply[A](head: A, tail: A*): MyList[A] = | |
(head +: tail).foldRight(empty[A])(Cons(_, _)) | |
case class Empty[A]() extends MyList[A] | |
case class Cons[A](head: A, tail: MyList[A]) extends MyList[A] | |
} | |
sealed trait Maybe[A] | |
object Maybe { | |
def none[A]: Maybe[A] = Empty() | |
def apply[A](a: A): Maybe[A] = | |
if (a == null) none else Just(a) | |
case class Empty[A]() extends Maybe[A] | |
case class Just[A](a: A) extends Maybe[A] | |
} | |
/// This technique is called "ad hoc polymorphism" | |
/// It is the main principle behind type classes which is not a part of the standard Scala library but is very easy to implement | |
trait Mapper[F[_]] { | |
def map[A, B](fa: F[A])(f: A => B): F[B] | |
} | |
object Mapper { | |
val maybeMapper: Mapper[Maybe] = new Mapper[Maybe] { | |
def map[A, B](fa: Maybe[A])(f: A => B): Maybe[B] = fa match { | |
case Maybe.Just(a) => Maybe.Just(f(a)) | |
case Maybe.Empty() => Maybe.none | |
} | |
} | |
val myListMapper: Mapper[MyList] = new Mapper[MyList] { | |
def map[A, B](fa: MyList[A])(f: A => B): MyList[B] = fa match { | |
case MyList.Empty() => MyList.Empty() | |
case MyList.Cons(h, t) => MyList.Cons(f(h), map(t)(f)) | |
} | |
} | |
} | |
def addInt(i: Int): Int => Int = _ + i | |
def mapMaybeInt(f: Int => Int)(mi: Maybe[Int]): Maybe[Int] = | |
mi match { | |
case Maybe.Just(i0) => Maybe(f(i0)) | |
case n => n // none case | |
} | |
mapMaybeInt(addInt(2))(Maybe(1)) == Maybe(3) | |
// res0: Boolean = true | |
mapMaybeInt(addInt(2))(Maybe.none) == Maybe.none | |
// res1: Boolean = true | |
/// Before going to type classes, we need to understand how implicit works | |
// They come in several flavours: | |
// 1. Implicit parameters/arguments | |
// 2. Implicit classes | |
// 3. Implicit conversions | |
// Implicit parameters/arguments | |
// An implicit argument can be ommitted from a function call | |
// The missing parameter has to be provided | |
// The compiler will look for an implicit value of the same type and with the "implicit" keyword | |
// The compiler will look for the implicit as a local definitions | |
// if it is not found, it will look for an implicit value as an import | |
def add[A](a: A, b: A)(implicit combine: (A, A) => (A)): A = combine(a, b) | |
// First thing to look | |
implicit val addIntImplicit: (Int, Int) => Int = _ + _ | |
// addIntImplicit: (Int, Int) => Int = <function2> | |
implicit val addStringImplicit: (String, String) => String = _ + _ | |
// addStringImplicit: (String, String) => String = <function2> | |
// Second thing to look | |
/* object SomeObject { | |
implicit val addIntImplicit: (Int, Int) => Int = _ + _ | |
implicit val addStringImplicit: (String, String) => String = _ + _ | |
} | |
import SomeObject._ */ | |
// Third thing to look -> Companion objects | |
// This one is advantegous because it doesn't require an import at the call site | |
case class Bar(i: Int) | |
object Bar { | |
implicit val plusBar: (Bar, Bar) => Bar = (a, b) => Bar(a.i + b.i) | |
} | |
add(Bar(1), Bar(2)) | |
// res2: Bar = Bar(i = 3) | |
add(1, 2) | |
// res3: Int = 3 | |
add("a", "b") | |
// res4: String = "ab" | |
// Implicit conversions | |
// Allows to convert a type to another type implicitly | |
// ⚠️ Highly discouraged | |
case class Foo(i: Int) | |
implicit def int2Foo(i: Int): Foo = Foo(i) | |
val foo: Foo = 1 | |
// foo: Foo = Foo(i = 1) | |
// Implicit classes | |
// Allows to add methods to an existing type | |
// Resolution works the same as the other implicits | |
// They take only one parameter, the type to which the methods are added | |
// It could be defined as a package object, define locally or in a separate file | |
implicit class IntOps(i: Int) { | |
def isEven: Boolean = i % 2 == 0 | |
} | |
12.isEven | |
// res5: Boolean = true | |
// Implicits can be chained | |
class Foo2 | |
object Foo2 { | |
implicit val foo2: Foo2 = new Foo2 | |
} | |
class Bar2(fa: Foo2) | |
object Bar2 { | |
implicit def bar2(implicit fa: Foo2): Bar2 = new Bar2(fa) | |
} | |
def foobar(implicit b: Bar2) = b | |
foobar | |
// res6: Bar2 = repl.MdocSession$App$Bar2@6754a617 | |
// Context Boundings | |
// A context bounding is a type parameter that is used to constrain the type of a type parameter | |
trait Foo3[A] { | |
def foo: A | |
} | |
case class Bar3[A](a: A) | |
implicit def bar3[A](implicit fa: Foo3[A]): Bar3[A] = Bar3(fa.foo) | |
// Can be translated to: | |
implicit def bar3_implicitly[A: Foo3]: Bar3[A] = Bar3( | |
implicitly[Foo3[A]].foo | |
) // ‼️ Note you don't have an explicit type parameter "fa" here, | |
// that's why you need the "implicitly" keyword to get the value of the implicit parameter | |
// Let's build the Mapper type class with implicits | |
/// 1. Make `MapperTypeClass` instances implicits | |
trait MapperTypeClass[F[_]] { | |
def map[A, B](fa: F[A])(f: A => B): F[B] | |
def flatMap[A, B <: A](fa: F[A])(f: A => F[B]): F[B] | |
def ++[A, B >: A](fa: F[B], fb: F[B]): F[B] | |
} | |
object MapperTypeClass { | |
// Summoner pattern | |
// This is a way to create instances of MapperTypeClass | |
def apply[F[_]: MapperTypeClass]: MapperTypeClass[F] = | |
implicitly[MapperTypeClass[F]] | |
implicit val maybeMapper: MapperTypeClass[Maybe] = | |
new MapperTypeClass[Maybe] { | |
def map[A, B](fa: Maybe[A])(f: A => B): Maybe[B] = fa match { | |
case Maybe.Just(a) => Maybe.Just(f(a)) | |
case Maybe.Empty() => Maybe.none | |
} | |
def flatMap[A, B](fa: Maybe[A])(f: A => Maybe[B]): Maybe[B] = fa match { | |
case Maybe.Just(a) => f(a) | |
case Maybe.Empty() => Maybe.none | |
} | |
def ++[A, B >: A](fa: Maybe[B], fb: Maybe[B]): Maybe[B] = | |
(fa, fb) match { | |
case (Maybe.Just(a), Maybe.Just(b)) => Maybe.Just((a)) | |
case (Maybe.Just(a), Maybe.Empty()) => Maybe.Just(a) | |
case (Maybe.Empty(), Maybe.Just(b)) => Maybe.Just(b) | |
case (Maybe.Empty(), Maybe.Empty()) => Maybe.none | |
} | |
} | |
implicit val myListMapper: MapperTypeClass[MyList] = | |
new MapperTypeClass[MyList] { | |
def map[A, B](fa: MyList[A])(f: A => B): MyList[B] = fa match { | |
case MyList.Empty() => MyList.Empty() | |
case MyList.Cons(h, t) => MyList.Cons(f(h), map(t)(f)) | |
} | |
def flatMap[A, B](fa: MyList[A])(f: A => MyList[B]): MyList[B] = | |
fa match { | |
case MyList.Empty() => MyList.Empty() | |
case MyList.Cons(h, t) => f(h) ++ flatMap(t)(f) | |
} | |
def ++[A, B >: A](fa: MyList[B], fb: MyList[B]): MyList[B] = | |
(fa, fb) match { | |
case (MyList.Empty(), MyList.Empty()) => MyList.Empty() | |
case (MyList.Empty(), MyList.Cons(h, t)) => MyList.Cons(h, t) | |
case (MyList.Cons(h, t), MyList.Empty()) => MyList.Cons(h, t) | |
case (MyList.Cons(h, t), MyList.Cons(h2, t2)) => | |
MyList.Cons(h, t ++ MyList.Cons(h2, t2)) | |
} | |
} | |
implicit class syntaxOps[F[_]: MapperTypeClass, A](fa: F[A]) { | |
def map[B](f: A => B): F[B] = implicitly[MapperTypeClass[F]].map(fa)(f) | |
def flatMap[B <: A](f: A => F[B]): F[B] = | |
implicitly[MapperTypeClass[F]].flatMap(fa)(f) | |
def ++[B >: A](fb: F[A]) = implicitly[MapperTypeClass[F]].++(fa, fb) | |
} | |
} | |
// Bound addTypeClass so that it can be used only if there's an implicit instance of `MapperTypeClass` for `F[_]` | |
// from: def addTypeClass[F[_]](fi: F[Int], mm: MapperTypeClass[F]): F[Int] = mm.map(fi)(_ + 1) | |
def addTypeClass[F[_]: MapperTypeClass](fi: F[Int]): F[Int] = | |
implicitly[MapperTypeClass[F]].map(fi)(_ + 1) | |
import MapperTypeClass._ // Importing for line 186, line 187 will work without it due to implicit resolution rules | |
Maybe(1).map(_ + 1) == Maybe(2) | |
// res7: Boolean = true | |
addTypeClass(MyList(1, 2, 3)) == MyList(2, 3, 4) | |
// res8: Boolean = true | |
// With the summoner pattern, we can create instances of MapperTypeClass without having to define them explicitly with the "implicitly" keyword 👀 | |
def addTypeClass2[F[_]: MapperTypeClass](fi: F[Int]): F[Int] = | |
MapperTypeClass[F].map(fi)(_ + 1) | |
addTypeClass2(MyList(1, 2, 3)) == MyList(2, 3, 4) | |
// res9: Boolean = true | |
MyList(1, 2, 3).flatMap(x => MyList(x, x)) == MyList(1, 1, 2, 2, 3, 3) | |
// res10: Boolean = true | |
MyList(1, 2, 3) ++ MyList(4, 5, 6) == MyList(1, 2, 3, 4, 5, 6) | |
// res11: Boolean = true | |
MyList(1, 2, 3) ++ MyList("4, 5, 6") == MyList(1, 2, 3, "4, 5, 6") | |
// res12: Boolean = true | |
// A type class is a type that defines a set of operations on types, is often referred to as a type class instance | |
// Type classes should live in the type class companion object or the instance type's companion object so you don't have to import them | |
// All type classes must comply with the following laws: | |
// 1. Identity | |
/// fa.map(identity) == fa | |
// 2. Composition | |
/// fa.map(a => f(g(a))) == fa.map(g).map(f) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment