Skip to content

Instantly share code, notes, and snippets.

@sagoez
Last active April 20, 2022 23:29
Show Gist options
  • Select an option

  • Save sagoez/e7090029a8c27e61c821f3efad3ff65b to your computer and use it in GitHub Desktop.

Select an option

Save sagoez/e7090029a8c27e61c821f3efad3ff65b to your computer and use it in GitHub Desktop.

Revisions

  1. sagoez revised this gist Apr 20, 2022. 1 changed file with 79 additions and 46 deletions.
    125 changes: 79 additions & 46 deletions typeclass.worksheet.scala
    Original file line number Diff line number Diff line change
    @@ -6,17 +6,17 @@ object MyList {
    def apply[A](head: A, tail: A*): MyList[A] =
    (head +: tail).foldRight(empty[A])(Cons(_, _))

    case class Empty[A]() extends MyList[A]
    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()
    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 Empty[A]() extends Maybe[A]
    case class Just[A](a: A) extends Maybe[A]
    }

    @@ -27,7 +27,7 @@ trait Mapper[F[_]] {
    }

    object Mapper {
    val maybeMapper: Mapper[Maybe] = new Mapper[Maybe] {
    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
    @@ -51,12 +51,9 @@ def mapMaybeInt(f: Int => Int)(mi: Maybe[Int]): Maybe[Int] =

    mapMaybeInt(addInt(2))(Maybe(1)) == Maybe(3)
    // res0: Boolean = true
    // res0: Boolean = true
    // res0: Boolean = true

    mapMaybeInt(addInt(2))(Maybe.none) == Maybe.none
    // res1: Boolean = true
    // res1: Boolean = true
    // res1: Boolean = true

    /// Before going to type classes, we need to understand how implicit works

    @@ -76,21 +73,17 @@ mapMaybeInt(addInt(2))(Maybe.none) == Maybe.none
    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>
    // addIntImplicit: (Int, Int) => Int = <function2>
    implicit val addIntImplicit: (Int, Int) => Int = _ + _
    // addIntImplicit: (Int, Int) => Int = <function2>

    implicit val addStringImplicit: (String, String) => String = _ + _
    // addStringImplicit: (String, String) => String = <function2>
    // addStringImplicit: (String, String) => String = <function2>
    // 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
    @@ -102,17 +95,12 @@ object Bar {

    add(Bar(1), Bar(2))
    // res2: Bar = Bar(i = 3)
    // res2: Bar = Bar(i = 3)
    // res2: Bar = Bar(i = 3)

    add(1, 2)
    // res3: Int = 3
    // res3: Int = 3
    // res3: Int = 3

    add("a", "b")
    // res4: String = "ab"
    // res4: String = "ab"
    // res4: String = "ab"

    // Implicit conversions
    // Allows to convert a type to another type implicitly
    @@ -123,8 +111,6 @@ implicit def int2Foo(i: Int): Foo = Foo(i)

    val foo: Foo = 1
    // foo: Foo = Foo(i = 1)
    // foo: Foo = Foo(i = 1)
    // foo: Foo = Foo(i = 1)

    // Implicit classes
    // Allows to add methods to an existing type
    @@ -138,8 +124,6 @@ implicit class IntOps(i: Int) {

    12.isEven
    // res5: Boolean = true
    // res5: Boolean = true
    // res5: Boolean = true

    // Implicits can be chained
    class Foo2
    @@ -155,8 +139,6 @@ object Bar2 {
    def foobar(implicit b: Bar2) = b
    foobar
    // res6: Bar2 = repl.MdocSession$App$Bar2@6754a617
    // res6: Bar2 = repl.MdocSession$App$Bar2@f75af66
    // res6: Bar2 = repl.MdocSession$App$Bar2@6af616cb

    // Context Boundings
    // A context bounding is a type parameter that is used to constrain the type of a type parameter
    @@ -168,62 +150,113 @@ 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,
    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 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))

    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)
    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
    // res7: Boolean = true

    addTypeClass(MyList(1, 2, 3)) == MyList(2, 3, 4)
    // res8: Boolean = true
    // 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)
    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)
    /// fa.map(a => f(g(a))) == fa.map(g).map(f)
  2. sagoez created this gist Apr 6, 2022.
    229 changes: 229 additions & 0 deletions typeclass.worksheet.scala
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,229 @@
    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
    // res0: Boolean = true
    // res0: Boolean = true
    mapMaybeInt(addInt(2))(Maybe.none) == Maybe.none
    // res1: Boolean = true
    // res1: Boolean = true
    // 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>
    // addIntImplicit: (Int, Int) => Int = <function2>
    // addIntImplicit: (Int, Int) => Int = <function2>
    implicit val addStringImplicit: (String, String) => String = _ + _
    // addStringImplicit: (String, String) => String = <function2>
    // addStringImplicit: (String, String) => String = <function2>
    // 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)
    // res2: Bar = Bar(i = 3)
    // res2: Bar = Bar(i = 3)

    add(1, 2)
    // res3: Int = 3
    // res3: Int = 3
    // res3: Int = 3
    add("a", "b")
    // res4: String = "ab"
    // res4: String = "ab"
    // 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)
    // foo: Foo = Foo(i = 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
    // res5: Boolean = true
    // 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
    // res6: Bar2 = repl.MdocSession$App$Bar2@f75af66
    // res6: Bar2 = repl.MdocSession$App$Bar2@6af616cb

    // 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]
    }

    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
    }
    }
    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))
    }
    }

    implicit class syntaxOps[F[_]: MapperTypeClass, A](fa: F[A]) {
    def map[B](f: A => B): F[B] = implicitly[MapperTypeClass[F]].map(fa)(f)
    }
    }

    // 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
    // res7: Boolean = true
    addTypeClass(MyList(1, 2, 3)) == MyList(2, 3, 4)
    // res8: Boolean = true
    // 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
    // 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)