scala type classes: basics and more
TRANSCRIPT
Type Classes: Basics and More
Sukant Hajra
April 10, 2015
Sukant Hajra Type Classes: Basics and More April 10, 2015 1 / 36
Outline
1 Introduction
2 Motivation by Examples
3 Encoding type classes in Scala
4 A Glance at Scalaz (Scaladoc, Demo)
5 Wrap-up
Sukant Hajra Type Classes: Basics and More April 10, 2015 2 / 36
Section 1
Introduction
Sukant Hajra Type Classes: Basics and More April 10, 2015 3 / 36
A show of hands
How many of you are very new to type classes?
This talk is yours, so interrupt and participate!
Sukant Hajra Type Classes: Basics and More April 10, 2015 4 / 36
Some background
Type classes
come from Haskell
use the word �class� di�erently from OO languages
started o� a way to improve ad hoc polymorphism
evolved into a modular way to abstract invariants/contracts.
Sukant Hajra Type Classes: Basics and More April 10, 2015 5 / 36
Types of polymorphism
parametric polymorphism (generics)
Foo[A]
subtype polymorphism
Foo extends Bar
ad hoc polymorphism (overloading)
def baz(foo: Foo): Baz
def baz(bar: Bar): Baz
Sukant Hajra Type Classes: Basics and More April 10, 2015 6 / 36
Section 2
Motivation by Examples
Sukant Hajra Type Classes: Basics and More April 10, 2015 7 / 36
Before we start with examples
Don't get caught up in the details
We'll cover details in a later section
Sukant Hajra Type Classes: Basics and More April 10, 2015 8 / 36
Problem: ad hoc polymorphism
given
def size(foo: Foo): Int
def size(bar: Bar): Int
hard to generalize/extend, have to continue overloading
def size_doubled(foo: Foo) = size(foo: Foo) * 2
def size_doubled(bar: Bar) = size(bar: Bar) * 2
or maybe use a sum type like Either
def size_doubled(fooOrBar: Either[Foo, Bar]) =
fooOrBar.fold(size(_), size(_)) * 2
Sukant Hajra Type Classes: Basics and More April 10, 2015 9 / 36
Solving with type classes: ad hoc polymorphism
we have an interface
trait HasSize[A] { def size(a: A): Int }
which we can use as a constraint
def sizeDoubled[A : HasSize](a: A) =
HasSize[A].size(a) * 2
but only if we have instances
implicit val fooHasSize: HasSize[Foo] =
new HasSize[Foo] { def size(f: Foo) = f.size }
implicit val barHasSize: HasSize[Bar] =
new HasSize[Bar] { def size(b: Bar) = b.size }
Note the nice separation of concerns.
Sukant Hajra Type Classes: Basics and More April 10, 2015 10 / 36
type classes catch errors statically
given
case class Foo(size: Int)
case class Bar(size: Int)
Example (using HasSize)
scala> sizeDoubled(Foo(5))
res0: Int = 5
scala> sizeDoubled(Bar(3))
res0: Int = 3
scala> sizeDoubled("wat?")
<console>:25: error: could not find implicit value for
evidence parameter of type HasSize[String]
sizeDoubled("wat?")
^
Sukant Hajra Type Classes: Basics and More April 10, 2015 11 / 36
Nicer error messages
Using @implicitNotFound on the type class interface
@annotation.implicitNotFound(
"instance of type class HasSize not found: ${A}")
trait HasSize[A] { def size(a: A): Int }
Example (we can get more human-friendly messages)
scala> sizeDoubled("wat?")
<console>:14: error: instance of type class HasSize
not found: String
sizeDoubled("wat?")
^
Sukant Hajra Type Classes: Basics and More April 10, 2015 12 / 36
Problem: equality
A typical equals
class This {
override def equals(that: Any): Boolean =
if (! that.isInstanceOf[This])
false
else
...
}
leads to obligatory boilerplate
the compiler should sometimes prevent equality comparison
Sukant Hajra Type Classes: Basics and More April 10, 2015 13 / 36
Solving with type classes: equality
we have an interface
trait Equal[A] { def eq(a1: A, a2: A): Boolean }
which we can use as a constraint
def member[A : Equal](as: List[A], a: A) =
as.foldLeft(false) { _ || Equal[A].eq(_, a) }
but only if we have instances
implicit val fooEqual: Equal[Foo] =
new Equal[Foo] {
def eq(f1: Foo, f2: Foo) = f1.x == f2.x
}
Sukant Hajra Type Classes: Basics and More April 10, 2015 14 / 36
Using type classes for type enrichment
EqualOps.scala
implicit class EqualOps[A : Equal](a1: A) {
def ===(a2: A): Boolean = Equal[A].eq(a1, a2)
}
implicit vals/defs for type class instances
implicit classes for type enrichment
Sukant Hajra Type Classes: Basics and More April 10, 2015 15 / 36
Type class equality is safer
Example (Using the equality type class)
scala> new Foo(1) == new Foo(1)
res0: Boolean = false
scala> new Foo(1) == "wat?"
res1: Boolean = false
scala> new Foo(1) === new Foo(1)
res2: Boolean = true
scala> new Foo(1) === "wat?"
<console>:24: error: type mismatch;
found : String("wat?")
required: Foo
new Foo(1) === "wat?"
Sukant Hajra Type Classes: Basics and More April 10, 2015 16 / 36
Example: Monoid
we have an interface
trait Monoid[A] {
def mappend(a1: A, a2: A): A
def mempty: A
}
which we can use as a constraint
def empty[A : Monoid]: A = Monoid[A].mempty
implicit class MonoidOps[A : Monoid](a1: A) {
def |+|(a2: A): A = Monoid[A].mappend(a1, a2)
}
Sukant Hajra Type Classes: Basics and More April 10, 2015 17 / 36
Monoid instances
here's some monoid instances
implicit val stringMonoid: Monoid[String] =
new Monoid[String] {
def mappend(s1: String, s2: String) = s1 + s2
def mempty = ""
}
implicit def pairMonoid[A : Monoid, B : Monoid]
: Monoid[(A, B)] =
new Monoid[(A, B)] {
def mappend(p1: (A, B), p2: (A, B)) =
(p1._1 |+| p2._1, p1._2 |+| p2._2)
def mempty = (empty[A], empty[B])
}
Sukant Hajra Type Classes: Basics and More April 10, 2015 18 / 36
Automatic derivation
Example (Using the equality type class)
scala> "a" |+| "1"
res0: String = a1
scala> ("a", "b") |+| ("1", "2")
res1: (String, String) = (a1,b2)
scala> ("a", ("b", "c")) |+| ("1", ("2", "3"))
res2: (String, (String, String)) = (a1,(b2,c3))
scala> empty[String]
res3: String = ""
scala> empty[(String, (String, String))]
res4: (String, (String, String)) = ("", ("", ""))
Sukant Hajra Type Classes: Basics and More April 10, 2015 19 / 36
Relation to objects
dispatch of interfaces (though static, not dynamic)
same principles, but more
single responsibilityopen/close principleinterface segregationstrong contracts
Sukant Hajra Type Classes: Basics and More April 10, 2015 20 / 36
Lawful type classes: strong contracts
Equality Laws
For all a, b, and c of any type with an Equal constrant
Re�exivity: eq(a, a)
Symmetry: eq(a, b) ≡ eq(b, a)
Transitivity: eq(a, b) ∧ eq(b, c) ≡ eq(a, c)
Monoid Laws
For all a, b, and c of any type with a Monoid constrant
Left Identity: mappend(mempty , a) ≡ a
Right Identity: mappend(a,mempty) ≡ a
Associativity:mappend(a,mappend(b, c)) ≡ mappend(mappend(a, b), c)
Sukant Hajra Type Classes: Basics and More April 10, 2015 21 / 36
What a type class is
De�nition (Type Class)
A type class is a constraint/interface that can be speci�ed on a typesignature for a parametric type. At the call site, the correspondinginstance/implementation is statically (at compile-time) derived, provided,and guaranteed to always be the same.
Ideally
Type classes should be lawful.
Sukant Hajra Type Classes: Basics and More April 10, 2015 22 / 36
Example: Functor
we have an interface
trait Functor[F[_]] {
def fmap[A](fa: F[A], f: A=>B): F[B]
}
with the following laws
Identity: fmap(fa, identity) ≡ fa
Composition: fmap(fa, a andThen b) ≡ fmap(fmap(fa, a), b)
Sukant Hajra Type Classes: Basics and More April 10, 2015 23 / 36
Is this a good type class?
type class for an isomorphism
trait Iso[A, B] {
def to(a: A): B
def from(b: B): A
}
it has laws
to(from(b)) ≡ bfrom(to(a)) ≡ a
but is it going to be unique?
Sukant Hajra Type Classes: Basics and More April 10, 2015 24 / 36
Summary of type class bene�ts
an improved alternative to ad hoc polymorphism
nicely externally extensible (separation of concerns)
automatic derivation of type class instances
strong invariants with lawful type classes
Sukant Hajra Type Classes: Basics and More April 10, 2015 25 / 36
Section 3
Encoding type classes in Scala
Sukant Hajra Type Classes: Basics and More April 10, 2015 26 / 36
Uses and abuses of implicits
Good usage:
type enrichment
type class encoding
Easily abused usage:
type conversion (beyond scope, but basically an implicit function)
Terrible usage:
dependency injection framework encoding
Sukant Hajra Type Classes: Basics and More April 10, 2015 27 / 36
Implicits, illustrated
In Scala, we encode type classes with implicits, so let's cover those �rst.
Example (implicits in Scala)
scala> case class Foo(name: String)
scala> implicit val defaultFoo = Foo("default")
scala> def fooPair(i: Int)(implicit foo: Foo) = (i, foo)
scala> fooPair(3)
res0: (Int, Foo) = (3,Foo(default))
The implicit parameter doesn't need an argument explicitly passed if a valuefor the type has been implicitly de�ned and can be found by the compiler.
Sukant Hajra Type Classes: Basics and More April 10, 2015 28 / 36
Implicit extras
useful, and in the standard library
def implicitly[A](implicit a: A): A = a
context bound syntax sugar; these are the same
def foo[A](implicit ev: TC[A]) = ...
def foo[A : TC] = {
val ev = implicitly[TC[A]]
...
}
Sukant Hajra Type Classes: Basics and More April 10, 2015 29 / 36
Implicit extra extras
using implicitly with context bounds is tedious
def foo[A : HasSize] = {
... implicitly[HasSize[A]].size ...
}
we can get better syntax
def foo[A : HasSize] = {
... HasSize[A].size ...
}
with an apply method on the type class's companion object
object HasSize {
def apply[A](implicit ev: HasSize[A]): HasSize[A] = ev
}
Sukant Hajra Type Classes: Basics and More April 10, 2015 30 / 36
Implicit scope resolution for type class encoding
Implicit scope resolution is kind of complex.
For type class instances keep it simple and put them in
companion objects of the types you controlpackage objects for types you don't control
When searching for an implicit A[B], companion objects for both A
and B will be searched.
Remember to have one and only one instance to �nd (otherwise you'renot encoding a proper type class!)
Sukant Hajra Type Classes: Basics and More April 10, 2015 31 / 36
Example encoding with companion objects
HasSize.scala
trait HasSize[A] { def size[A](a: A): Int }
object HasSize {
@inline
def apply[A](implicit ev: HasSize[A]): HasSize[A] = ev
implicit def listHasSize[A]: HasSize[List[A]] =
new HasSize[List[A]] { def size(l: List[A]) = l.size }
}
Foo.scala
case class Foo(size: Int)
object Foo {
implicit val hasSize: HasSize[Foo] =
new HasSize[Foo] { def size(f: Foo) = f.size }
}
Sukant Hajra Type Classes: Basics and More April 10, 2015 32 / 36
Example encoding with package objects
Instances.scala
trait Instances {
implicit def listHasSize[A]: HasSize[List[A]] =
new HasSize[List[A]] { def size(l: List[A]) = l.size }
}
object Instances extends Instances
package.scala
package object myproject extends Instances
useful when you control neither the type class nor the data type
to avoid compilation complexity, some people never put instances ontype class companion objects (just the data type)
Sukant Hajra Type Classes: Basics and More April 10, 2015 33 / 36
Section 4
A Glance at Scalaz (Scaladoc, Demo)
Sukant Hajra Type Classes: Basics and More April 10, 2015 34 / 36
Section 5
Wrap-up
Sukant Hajra Type Classes: Basics and More April 10, 2015 35 / 36
Thanks!
Any questions? Comments?
References
Wadler, Blott, How to make ad-hoc polymorphism less ad hoc
Hudak, Hughes, Jones, Wadler, A History of Haskell: Being Lazy With
Class
Kmett, Type Classes vs. the World,https://youtu.be/hIZxTQP1ifo
Yang, Type classes: con�uence, coherence, and global uniqueness
Jones, Jones, Meijer, Type classes: an exploration of the design space
Sukant Hajra Type Classes: Basics and More April 10, 2015 36 / 36