Skip to content

Typeclass

  • Mechanism
    • TypeClass is parameterized trait
    • TypeClass Instance is a concrete implementation of TypeClass for a particular Type tagged with implicit
    • Type Class Interface is functionality/generic method(s) that accepts Type Class instance
      • Interface Object define Objects with methods that accept a type class instance implicitly
      • Interface Syntax defines extension method by providing implicit class definition
  • Scala checks for an implicit instance of typeclass TC[T] in companion objects of both, TC and T
  • Best practice: Use context bound syntax and define a companion object to instantiate typeclass
  • implicit definitions cannot be at package level, but must be placed in an object or a trait
  • Avoid structural types because they use reflection at run-time.
  • def can be overridden with val but not the other way around
  • implicit class are syntactic sugar for def + class
  • Syntactic Sugar
    // following two are equivalent
    def combineAll[A](list: List[A])(implicit A: Monoid[A]): A = ???
    def combineAll[A : Monoid](list: List[A]): A = ???   // context-bound syntax sugar
    
    // context bound syntax sugar requires use of implicitly
    def implicitly[A](implicit ev: A): A = ev  // this is already defined in std lib
    def combineAll[A : Monoid](list: List[A]): A =
        list.foldRight(implicitly[Monoid[A]].empty)(implicitly[Monoid[A]].combine)
    
    // Instead of calling implicitly everywhere, most cats typeclasses define
    object Monoid {
        def apply[A : Monoid]: Monoid[A] = implicitly[Monoid[A]]
    }
    
    def combineAll[A : Monoid](list: List[A]): A =
        list.foldRight(Monoid[A].empty)(Monoid[A].combine)
    

using and given

  • there can be multiple parameters list with using, and can be mixed with regular parameter lists
    • using parameters don't need to be named, you can use summon[Ordering[Int]]
  • given can be instances or an alias. Either way it's initialized only the first time it is used
    // given instance
    object Ordering:
        given Int: Ordering[Int] with
            def compare(x: Int, y: Int): Int = ???
    
    // given alias
    object IntOrdering extends Ordering[Int]:
        def compare(x: Int, y: Int): Int = ???
    
    given intOrdering: Ordering[int] = IntOrdering
    
  • given instances or aliases don't need to be named
    object Ordering:
        given Ordering[Int] with
            def compare(x: Int, y: Int): Int = ???
    
    object IntOrdering extends Ordering[Int]:
        def compare(x: Int, y: Int): Int = ???
    
    given Ordering[int] = IntOrdering
    
  • sub-class given instances are at higher priority than parent given instances
    // priority
    class General()
    class Specific() extends General()
    
    given general: General = General()
    given specific: Specific = Specific()
    
    summon[General]  // picks given specific
    
  • conditional given definition has a using clause: a given definition exists only if some other given definition is available
    given orderingList[A](using ord: Ordering[A]): Ordering[List[A]] with ...
    
  • extension methods for T are applicable if:
    • visible in the current scope (defined, inherited, imported)
    • defined in associated object of T
    • defined in given instance of T
      trait Ordering[A]:
          def compare(x: A, y: A): Int
          extension (lhs: Rational)
              def < (rhs: A): Boolean = compare(lhs, rhs) < 0
      
  • implicit-conversion can be accomplished using given and scala.Coversion
    • at most one implicit conversion is done (cannot chain)
    • requires import scala.language.implicitConversions

Examples

  • Examples:
    trait Serialize[T, O] {
        def serialize(t: T): O
    }
    implicit class SerializeOpt[A](val self: A) extends AnyVal {
        def serialize[O](implicit ser: Serialize[A, O]): O = ser.serialize(self)
    }
    
    class Node(val value: String)
    object Node {
        type Json = String
        type Length = Int
        implicit val jsonSer: Serialize[Node, Json] = new Serialize[Node, Json] {
            def serialize(t: Node): Json = s"{'value': '${t.value}'}"
        }
        implicit val lengthSer: Serialize[Node, Length] = new Serialize[Node, Length] {
            def serialize(t: Node): Length = t.value.length
        }
    }
    import Node._
    new Node("hi").serialize[Json] // {'value': 'hi'}
    new Node("hi").serialize[Length] // 2
    

Tagless Final

  • Algebra: a HKT trail with a set of operations (methods)
  • tagless-final consists of
    • dsl aka algebra using trait with HKT F[_]
    • interpreter that provides implementation of the algebra, with constraints on the HKT
    • programs (expression) which is constructed using algebras
  • provides flexibility with different interpreters without having to modify programs
  • Program accepts an interpreter
  • Initial encoding consists of dsl which is made up of ADT sum type, interpreter and program
  • sealed abstract case class private (...) modifiers
    • abstract suppresses generation of copy and apply methods
    • sealed prevent extending the class
    • private prevent instantiation
  • The goal of algebra is to allow a carrier type (e.g. Option) to be converted into a some other type (e.g. using getOrElse) possibly after applying combinators (e.g. map, flatMap etc)

(Co)Yoneda

  • Yoneda Converts a functor to a lazy functor such that
    1. multiple map invocations are deferred and composed
    2. when run, executes a single composed map on the underlying functor
  • Coyoneda is similar to Yoneda except the enforcement that the underlying type needs to be a functor is enforced at run method instead of typeclass definition
    • Creates a functor of any type constructor S that takes a single parameter. Coyoneda[S[_], ?]
    • facilitates deriving a free monad which requires a functor

Free Monad

  • turns any functor into a monad
  • intuition: represents a computation as a data structure
    • In Free[F, A]; think Free as the program, F as the algebra and A is the value produced by the program
      sealed trait Console[A]
      case class ReadLine[A](value: String => A) extends Console[A]
      case class PrintLine[A](line: String, value: A) extends Console[A]
      
      type Dsl[A] = Free[Console, A]
      def readLine: Dsl[String] = ReadLine(identity)
      def printLine(line: String): Dsl[Unit] = PrintLine(line, ())
      
      val program =
          for {
              line <- readLine
              _ <- printLine("You wrote " + line)
          } yield ()
      
  • data structures can be introspected, transformed etc
  • can have more than one interpreter to evaluate different results

Cofree (Monad)

  • represents co-inductive rather than inductive process
  • inductive process starts far from base state and tries to reach a base, simpler state (e.g. given base state as fact(1) = 0, fact(n) = n * fact(n-1) tries to reach the base state)
  • co-inductive process starts from a base state and moves away from base state to generate other states (potentially infinite) E.g.
    • given base states of fib(0) = 0; fib(1) = 1 generate fib(n) = fib(n-1) + fin(n-2)
    • Stream.unfold of fs2 that generates a stream from an initial state
  • scala final case class Cofree[F[_], A](head: A, tail: F[Cofree[F, A]]) // F is a Functor
    • head represents base state, and Cofree[F, A] represents some state that is derived from the base state
    • tail represents the recursive data structure, and it's contained in F to indicate it is lazy, and can be infinite
    • intuition #1: Cofree[F,A] is a co-inductive process that generates As using effect F
    • intuition #2: Cofree[F,A] is a current postion A on a landscape that requires effect F to a new position.
  • Cofree are typically used to represent streams, that can be infinite ```scala type Pipe[F[], A, B] = Cofree[Kliesli[F, A, ?], B] type Source[F[], A] = Pipe[F, Unit, A] type Sink[F[_], A] = Pipe[F, A, Unit]