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
implicitclass definition
- TypeClass is parameterized
- Scala checks for an
implicitinstance of typeclassTC[T]in companion objects of both,TCandT - Best practice: Use context bound syntax and define a companion object to instantiate typeclass
implicitdefinitions cannot be at package level, but must be placed in anobjector atrait- Avoid structural types because they use reflection at run-time.
defcan be overridden withvalbut not the other way aroundimplicit classare syntactic sugar fordef+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 listsusingparameters don't need to be named, you can usesummon[Ordering[Int]]
givencan be instances or an alias. Either way it's initialized only the first time it is usedgiveninstances or aliases don't need to be named- sub-class
giveninstances are at higher priority than parentgiveninstances - conditional
givendefinition has ausingclause: agivendefinition exists only if some othergivendefinition is available - extension methods for T are applicable if:
- visible in the current scope (defined, inherited, imported)
- defined in associated object of T
- defined in
giveninstance of T
- implicit-conversion can be accomplished using
givenandscala.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
- dsl aka algebra using trait with HKT
- 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 (...)modifiersabstractsuppresses generation ofcopyandapplymethodssealedprevent extending the classprivateprevent instantiation
- The goal of algebra is to allow a carrier type (e.g.
Option) to be converted into a some other type (e.g. usinggetOrElse) possibly after applying combinators (e.g.map,flatMapetc)
(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
Sthat takes a single parameter.Coyoneda[S[_], ?] - facilitates deriving a free monad which requires a functor
- Creates a functor of any type constructor
Free Monad¶
- turns any functor into a monad
- intuition: represents a computation as a data structure
- In
Free[F, A]; thinkFreeas the program,Fas the algebra andAis the value produced by the programsealed 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 ()
- In
- 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) = 1generatefib(n) = fib(n-1) + fin(n-2) Stream.unfoldoffs2that generates a stream from an initial state
- given base states of
scala final case class Cofree[F[_], A](head: A, tail: F[Cofree[F, A]]) // F is a Functorheadrepresents base state, andCofree[F, A]represents some state that is derived from the base statetailrepresents the recursive data structure, and it's contained inFto indicate it is lazy, and can be infinite- intuition #1:
Cofree[F,A]is a co-inductive process that generatesAs using effectF - intuition #2:
Cofree[F,A]is a current postionAon a landscape that requires effectFto a new position.
Cofreeare 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]