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

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
  • Unlike tagless-final, in initial-encoding, Interpreter accepts a program to run
  • 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

  • multiple map invocations are deferred and composed

  • 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
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]