KEMBAR78
Generic Functional Programming with Type Classes | PDF
Generic Functional Programming
with Type Classes
Tapio Rautonen
Tapio Rautonen
@trautonen
De facto visibility optimization
technology for video game industry
and world leading 3D optimization and
delivery platform
Bending Scala and other technologies
to provide the most immersive 3D
experience from the cloud
The following presentation shows some raw
Scala code and might even discover the M-word.
In case of dizzy vision or fuzzy sounds in your
head, cover your eyes and ears and run.
The presenter is not responsible if the audience
turns functional programmers and introduce
Haskell to their next project.
Generic is not specific
trait ContactRepository {
def getContact(name: String): Future[Option[Contact]]
def updateContact(contact: Contact): Future[Contact]
}
trait Calculus {
def sum(x: Double, y: Double): Either[ArithmeticError, Double]
def divide(x: Double, y: Double): Either[ArithmeticError, Double]
def mean(xs: Iterable[Double]): Either[ArithmeticError, Double]
}
Functional programming is not object oriented
class MissileSilo(control: MissileControl) {
var missileCount: Int = 3
def launchMissile(): Unit = {
missileCount = missileCount - 1
control.ignite()
}
}
Type classes are not ad hoc inheritance
abstract class Animal {
var name: String = null
def utter(): Unit
override def equals(obj: Any): Boolean =
obj.isInstanceOf[Animal] && obj.asInstanceOf[Animal].name == name
}
class Dog extends Animal {
override def utter(): Unit = print("Wuf!")
override def equals(obj: Any): Boolean =
obj.isInstanceOf[Dog] && super.equals(obj)
}
Now we know what this is not about
but it’s pretty useless as is, so let’s rewind
Type classes
★ Enable ad-hoc polymorphism, aka overloading, without
inheritance
★ Type class is sort of an interface that defines some
behaviour and laws
★ Haskell introduced type classes to provide arithmetic and
equality operator overloading
★ In Scala type classes utilize traits, implicit context and
some syntactic sugar
Type classes - equality interface
trait Equality[T] {
def equals(a: T, b: T): Boolean
}
object Equality {
def equivalent[T](a: T, b: T)(implicit ev: Equality[T]): Boolean =
ev.equals(a, b)
def equivalent[T: Equality](a: T, b: T): Boolean =
implicitly[Equality[T]].equals(a, b)
}
Type classes - equality implementation
implicit val doubleEquality = new Equality[Double] {
override def equals(a: Double, b: Double): Boolean =
a == b
}
implicit val durationEquality = new Equality[Duration] {
override def equals(a: Duration, b: Duration): Boolean =
a.toNanos == b.toNanos
}
equivalent(1.3, 1.2)
//res0: false
equivalent(60.seconds, 1.minute)
//res1: true
Type classes - deserialization
trait Decoder[T] {
def decode(value: String): Either[DecodingError, T]
}
object Decoders {
implicit val jsonDecoder = new Decoder[Json] {
override def decode(value: String): Either[DecodingError, Json] =
JsonParser.parse(value) }
implicit val xmlDecoder = new Decoder[Xml] {
override def decode(value: String): Either[DecodingError, Xml] =
XmlReader.readInput(new ByteInputStream(value.getBytes())) }
}
Type classes - request handler
class RequestHandler[D: Decoder] {
def handleRequest(request: Request): Response = {
implicitly[Decoder[D]].decode(request.body) match {
case Left(error) => Response(400, error.message)
case Right(body) => Response(200, body)
}
}
}
import Decoders.jsonDecoder
val jsonRequestHandler = new RequestHandler[Json]
Functional programming
★ Paradigm that concentrates on computing results rather
than performing actions
★ Is essentially referential transparency - expression always
evaluates to the same result in any context
★ Pure functions are deterministic and do not cause any
observable side effects
★ Relies on immutability which means that state cannot
change after it’s created
Functional programming - pure functions
def add(a: Int, b: Int): Int = a + b
def sum(list: List[Int]): Int = list match {
case Nil => 0
case head :: tail => head + sum(tail)
}
List(1, 2, 3, 4, 5).map(n => n + 1)
Functional programming - referential transparency
ab + cd == add(a, b) + add(c, d)
val file = File("/dev/random")
val data = file.readBytes(10)
data ++ data != file.readBytes(10) ++ file.readBytes(10)
val atom = new AtomicInteger()
val value = atom.incrementAndGet()
value + value != atom.incrementAndGet() + atom.incrementAndGet()
Not RT
Not RT
Functional programming - composing
def subtract(a: Int, b: Int): Int = a - b
def multiply(v: Int, n: Int): Int = v * n
val subtractOne = (a: Int) => subtract(a, 1)
val multiplyFiveTimes = (v: Int) => multiply(v, 5)
List(1, 2, 3, 4, 5)
.map(subtractOne andThen multiplyFiveTimes)
.filter(_ % 2 == 0)
//res0: List(0, 10, 20)
Generic
★ Generalize code with type system assistance, for example
parametric types, generics or templates
★ Higher kinded types are repetition of polymorphism with
types, abstracting types that abstract types
★ Apply the principle of least power to pick the least
powerful solution capable of solving your problem
★ Code that is easy to change and interpret differently in the
future
Generic - first-order
trait List[T] {
def isEmpty: Boolean
def head: T
def tail: List[T]
}
trait Monoid[A] {
def combine(x: A, y: A): A
def empty: A
}
Generic - composition
implicit val integerAddition: Monoid[Int] = new Monoid[Int] {
override def combine(x: Int, y: Int): Int = x + y
override def empty: Int = 0
}
def combineAll[A: Monoid](values: List[A]): A =
values.foldLeft(implicitly[Monoid[A]].empty)
(implicitly[Monoid[A]].combine)
Generic - higher order
trait Monad[F[_]] {
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
def pure[A](x: A): F[A]
}
def map[A, B, F[_]: Monad](fa: F[A])(f: A => B): F[B] =
implicitly[Monad[F]].flatMap(fa)
(a => implicitly[Monad[F]].pure(f(a)))
Generic - higher order implementation
implicit val listMonad = new Monad[List] {
def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B] = fa.flatMap(f)
def pure[A](x: A): List[A] = List(x)
}
implicit val streamMonad = new Monad[Stream] {
def flatMap[A, B](fa: Stream[A])(f: A => Stream[B]): Stream[B] =
fa.flatMap(f)
def pure[A](x: A): Stream[A] = Stream(x)
}
map(List(1, 2, 3, 4, 5))(_ + 1)
//res0: List(2, 3, 4, 5 ,6)
map(Stream(1, 2, 3, 4, 5))(_ + 1)
//res1: Stream(2, ?)
Tagless Final
★ Built on top of the previous patterns to create generic type
assisted domain specific languages
★ “Tagless” as there is no need for runtime tag checking to
perform type and error checking
★ “Final” as the interpretation is done in the target monad,
not deferred
★ Programs are expressions that are formed from operations
which are just functions
Tagless final - algebra and business logic
trait ContactRepository[F[_]] {
def get(name: String): F[Option[Contact]]
def update(contact: Contact): F[Contact]
}
class AddressBook[F[_]: Monad](cr: ContactRepository[F]) {
def updateAddress(n: String, addr: Address): F[Either[Error, Contact]] = {
cr.get(n) flatMap {
case None => Monad[F].pure(Left(NotFound(s"$n not found")))
case Some(c) => cr.update(c.copy(address = addr)).map(Right(_))
}
}
}
Tagless final - interpreters
class FutureInterpreter(db: Database) extends ContactRepository[Future] {
override def getContact(name: String): Future[Option[Contact]] =
db.findContact(name)
override def updateContact(contact: Contact): Future[Contact] =
db.updateContact(contact)
}
class IOInterpreter(dobbly: Dobbly) extends ContactRepository[IO] {
override def getContact(name: String): IO[Option[Contact]] =
dobbly.query(d"find contact name = $name")
override def updateContact(contact: Contact): IO[Contact] =
dobbly.query(d"update contact = $contact")
}
Tagless final - usage
val result: Future[Either[Error, Contact]] =
new AddressBook(new FutureInterpreter(db))
.updateContactAddress("John Doe", address)
val io: IO[Either[Error, Contact]] =
new AddressBook(new IOInterpreter(dobbly))
.updateContactAddress("John Doe", address)
val result: Future[Either[Error, Contact]] = io.transactionally().run()
All clear?
Scala is the gateway drug to Haskell

Generic Functional Programming with Type Classes

  • 1.
    Generic Functional Programming withType Classes Tapio Rautonen
  • 2.
    Tapio Rautonen @trautonen De factovisibility optimization technology for video game industry and world leading 3D optimization and delivery platform Bending Scala and other technologies to provide the most immersive 3D experience from the cloud
  • 3.
    The following presentationshows some raw Scala code and might even discover the M-word. In case of dizzy vision or fuzzy sounds in your head, cover your eyes and ears and run. The presenter is not responsible if the audience turns functional programmers and introduce Haskell to their next project.
  • 4.
    Generic is notspecific trait ContactRepository { def getContact(name: String): Future[Option[Contact]] def updateContact(contact: Contact): Future[Contact] } trait Calculus { def sum(x: Double, y: Double): Either[ArithmeticError, Double] def divide(x: Double, y: Double): Either[ArithmeticError, Double] def mean(xs: Iterable[Double]): Either[ArithmeticError, Double] }
  • 5.
    Functional programming isnot object oriented class MissileSilo(control: MissileControl) { var missileCount: Int = 3 def launchMissile(): Unit = { missileCount = missileCount - 1 control.ignite() } }
  • 6.
    Type classes arenot ad hoc inheritance abstract class Animal { var name: String = null def utter(): Unit override def equals(obj: Any): Boolean = obj.isInstanceOf[Animal] && obj.asInstanceOf[Animal].name == name } class Dog extends Animal { override def utter(): Unit = print("Wuf!") override def equals(obj: Any): Boolean = obj.isInstanceOf[Dog] && super.equals(obj) }
  • 7.
    Now we knowwhat this is not about but it’s pretty useless as is, so let’s rewind
  • 8.
    Type classes ★ Enablead-hoc polymorphism, aka overloading, without inheritance ★ Type class is sort of an interface that defines some behaviour and laws ★ Haskell introduced type classes to provide arithmetic and equality operator overloading ★ In Scala type classes utilize traits, implicit context and some syntactic sugar
  • 9.
    Type classes -equality interface trait Equality[T] { def equals(a: T, b: T): Boolean } object Equality { def equivalent[T](a: T, b: T)(implicit ev: Equality[T]): Boolean = ev.equals(a, b) def equivalent[T: Equality](a: T, b: T): Boolean = implicitly[Equality[T]].equals(a, b) }
  • 10.
    Type classes -equality implementation implicit val doubleEquality = new Equality[Double] { override def equals(a: Double, b: Double): Boolean = a == b } implicit val durationEquality = new Equality[Duration] { override def equals(a: Duration, b: Duration): Boolean = a.toNanos == b.toNanos } equivalent(1.3, 1.2) //res0: false equivalent(60.seconds, 1.minute) //res1: true
  • 11.
    Type classes -deserialization trait Decoder[T] { def decode(value: String): Either[DecodingError, T] } object Decoders { implicit val jsonDecoder = new Decoder[Json] { override def decode(value: String): Either[DecodingError, Json] = JsonParser.parse(value) } implicit val xmlDecoder = new Decoder[Xml] { override def decode(value: String): Either[DecodingError, Xml] = XmlReader.readInput(new ByteInputStream(value.getBytes())) } }
  • 12.
    Type classes -request handler class RequestHandler[D: Decoder] { def handleRequest(request: Request): Response = { implicitly[Decoder[D]].decode(request.body) match { case Left(error) => Response(400, error.message) case Right(body) => Response(200, body) } } } import Decoders.jsonDecoder val jsonRequestHandler = new RequestHandler[Json]
  • 13.
    Functional programming ★ Paradigmthat concentrates on computing results rather than performing actions ★ Is essentially referential transparency - expression always evaluates to the same result in any context ★ Pure functions are deterministic and do not cause any observable side effects ★ Relies on immutability which means that state cannot change after it’s created
  • 14.
    Functional programming -pure functions def add(a: Int, b: Int): Int = a + b def sum(list: List[Int]): Int = list match { case Nil => 0 case head :: tail => head + sum(tail) } List(1, 2, 3, 4, 5).map(n => n + 1)
  • 15.
    Functional programming -referential transparency ab + cd == add(a, b) + add(c, d) val file = File("/dev/random") val data = file.readBytes(10) data ++ data != file.readBytes(10) ++ file.readBytes(10) val atom = new AtomicInteger() val value = atom.incrementAndGet() value + value != atom.incrementAndGet() + atom.incrementAndGet() Not RT Not RT
  • 16.
    Functional programming -composing def subtract(a: Int, b: Int): Int = a - b def multiply(v: Int, n: Int): Int = v * n val subtractOne = (a: Int) => subtract(a, 1) val multiplyFiveTimes = (v: Int) => multiply(v, 5) List(1, 2, 3, 4, 5) .map(subtractOne andThen multiplyFiveTimes) .filter(_ % 2 == 0) //res0: List(0, 10, 20)
  • 17.
    Generic ★ Generalize codewith type system assistance, for example parametric types, generics or templates ★ Higher kinded types are repetition of polymorphism with types, abstracting types that abstract types ★ Apply the principle of least power to pick the least powerful solution capable of solving your problem ★ Code that is easy to change and interpret differently in the future
  • 18.
    Generic - first-order traitList[T] { def isEmpty: Boolean def head: T def tail: List[T] } trait Monoid[A] { def combine(x: A, y: A): A def empty: A }
  • 19.
    Generic - composition implicitval integerAddition: Monoid[Int] = new Monoid[Int] { override def combine(x: Int, y: Int): Int = x + y override def empty: Int = 0 } def combineAll[A: Monoid](values: List[A]): A = values.foldLeft(implicitly[Monoid[A]].empty) (implicitly[Monoid[A]].combine)
  • 20.
    Generic - higherorder trait Monad[F[_]] { def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] def pure[A](x: A): F[A] } def map[A, B, F[_]: Monad](fa: F[A])(f: A => B): F[B] = implicitly[Monad[F]].flatMap(fa) (a => implicitly[Monad[F]].pure(f(a)))
  • 21.
    Generic - higherorder implementation implicit val listMonad = new Monad[List] { def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B] = fa.flatMap(f) def pure[A](x: A): List[A] = List(x) } implicit val streamMonad = new Monad[Stream] { def flatMap[A, B](fa: Stream[A])(f: A => Stream[B]): Stream[B] = fa.flatMap(f) def pure[A](x: A): Stream[A] = Stream(x) } map(List(1, 2, 3, 4, 5))(_ + 1) //res0: List(2, 3, 4, 5 ,6) map(Stream(1, 2, 3, 4, 5))(_ + 1) //res1: Stream(2, ?)
  • 22.
    Tagless Final ★ Builton top of the previous patterns to create generic type assisted domain specific languages ★ “Tagless” as there is no need for runtime tag checking to perform type and error checking ★ “Final” as the interpretation is done in the target monad, not deferred ★ Programs are expressions that are formed from operations which are just functions
  • 23.
    Tagless final -algebra and business logic trait ContactRepository[F[_]] { def get(name: String): F[Option[Contact]] def update(contact: Contact): F[Contact] } class AddressBook[F[_]: Monad](cr: ContactRepository[F]) { def updateAddress(n: String, addr: Address): F[Either[Error, Contact]] = { cr.get(n) flatMap { case None => Monad[F].pure(Left(NotFound(s"$n not found"))) case Some(c) => cr.update(c.copy(address = addr)).map(Right(_)) } } }
  • 24.
    Tagless final -interpreters class FutureInterpreter(db: Database) extends ContactRepository[Future] { override def getContact(name: String): Future[Option[Contact]] = db.findContact(name) override def updateContact(contact: Contact): Future[Contact] = db.updateContact(contact) } class IOInterpreter(dobbly: Dobbly) extends ContactRepository[IO] { override def getContact(name: String): IO[Option[Contact]] = dobbly.query(d"find contact name = $name") override def updateContact(contact: Contact): IO[Contact] = dobbly.query(d"update contact = $contact") }
  • 25.
    Tagless final -usage val result: Future[Either[Error, Contact]] = new AddressBook(new FutureInterpreter(db)) .updateContactAddress("John Doe", address) val io: IO[Either[Error, Contact]] = new AddressBook(new IOInterpreter(dobbly)) .updateContactAddress("John Doe", address) val result: Future[Either[Error, Contact]] = io.transactionally().run()
  • 26.
    All clear? Scala isthe gateway drug to Haskell