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
- 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 lists usingparameters 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 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
giveninstances 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
giveninstances are at higher priority than parentgiveninstances
// priority
class General()
class Specific() extends General()
given general: General = General()
given specific: Specific = Specific()
summon[General] // picks given specific
- 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
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
givenandscala.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 (...)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
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 () -
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
scala
final case class Cofree[F[_], A](head: A, tail: F[Cofree[F, A]]) // F is a Functor
headrepresents 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