API design: using type classes
    and dependent types

             Ben Lever
             @bmlever

         ScalaSyd Episode #6
             11 July 2012
What I want
traitComp[A] { ... }

              Specifies the
           “computation” of a
             value of type ‘A’


val ii: Comp[Int] = ...

vali: Int = exec(ii)

                         “Execute” the
                         computation
                        specified by ‘ii’.
What would be cool

val ii: Comp[Int] = ...
valss: Comp[String] = ...



val (i: Int, s: String) = exec(ii, ss)

                                          “Execute” multiple
                                           computations of
                                         different types, then
                                         return “embedded”
                                           types as a tuple.
More generally

valaa: Comp[A] = ...
val bb: Comp[B] = ...
valcc: Comp[C] = ...
valdd: Comp[D] = ...

val (a: A, b: B, c: C, d: D) = exec(aa, bb, cc, dd)


                                                Ability to “execute” many
                                                       computations
                                                simultaneously and return
                                                  all “embedded” values
                                                 together retaining types.
But wait, there’s more
traitIOComp[A] { ... }

             Specifies the “IO
            computation” of a
             value of type ‘A’


val ii: IOComp[Int] = ...
                                                            Err[A]
vali: Err[Int] = exec(ii)                                      ||
                                                       Either[String, A])
        Either return the        “Execute” the IO
        computed value             computation
        or an error string        specified by ‘ii’.
And of course

val ii: IOComp[Int] = ...
valss: IOComp[String] = ...



val (i: Err[Int],
s: Err[String]) = exec(ii, ss)

                                 “Execute” multiple IO
                                    computations of
                                  different types, then
                                  return “embedded”
                                    types as a tuple.
Finally

valaa: Comp[A] = ...
val bb: IOComp[B] = ...
valcc: Comp[C] = ...
valdd: IOComp[D] = ...

val (a: A, b: Err[B], c: C, d: Err[D]) =
exec(aa, bb, cc, dd)


                                           “Execute” a mixture
                                            of normal and IO
                                              computations.
Teaser - the final API



defexec[R](v: R)(implicit runner: Runner[R]): runner.Out
My inspiration
  “Couldn’t you use HLists/KLists for this?”

           Miles Sabin’s Shapeless
               provided much
          direction and inspirations

But … I’m not going to talk about Shapeless 
The tricks



defexec[R](v: R)(implicit runner: Runner[R]): runner.Out

      Tuple                  Type                     Dependent
    sweetness               classes                     types
Trick #1: tuple sweetness

defecho[A](x: A) { prinln(x) }



echo((“good”, 47, true))

echo(“good”, 47, true)     // equivalent
Trick #2: type classes
Get the “size” of an object

size(3) // 3
size(“hello”)// 5
size(false)// 1
...
Size type class
traitSize[T] {
defapply(in: T): Int                         Type class
}

object Size {
defsize[S](v: S)(implicits: Size[S]): Int = s(v)

implicit defIntSize = newSize[Int] {
defapply(in: Int): Int = in
  }
implicit defBoolSize = newSize[Boolean] {
defapply(in: Boolean): Int = 1
                                                          Type class
                                                          instances
  }
implicit defStringSize = newSize[String] {
defapply(in: String): Int = in.length
  }
}
Under the hood
Implicit objects inserted by compiler

size(3)(IntSize)// 3
size(“hello”)(StringSize)// 5
size(false)(BoolSize)// 1
...
Expanding the example
Now, get the “size” of multiple objects at once



size(3, “hello”,false)// (3,5,1)
Runner type class
traitRunner[In, Out] {
defapply(in: In): Out
}

object Runner {
defsize[I, O](v: I)(implicitr: Runner[I, O]): O = r(v)

implicit def Tup1[T1](implicit s1: Size[T1]) =
new Runner[T1, Int] {
defapply(in: T1): Int = s1(in)                                    Referencing
  }                                                                  ‘Size’

implicit def Tup2[T1,T2](implicit s1: Size[T1], s2: Size[T2]) =
new Runner[(T1,T2), (Int,Int)] {
defapply(in: (T1,T2)): (Int,Int) = (s1(in._1), s2(in._2))
  }

implicit def Tup3[T1,T2,T3](implicit s1: Size[T1], ...
Under the hood
Implicit object inserted by compiler



size(3, “hello”,false)
(Tup3
IntSize,
StringSize,
BoolSize))
Aside: type class sugar

defsize[S](v: S)(implicitsizer: Size[S]): Int


is equivalent to
defsize[S : Size](v: S) : Int

                     Type class
                     constraint
‘size’ is easy
Runner instances know output types are always Ints


                    Returns Int


size(3, “hello”,false)// (3,5,1)

      Returns Int                 Returns Int
‘exec’ is harder
Output types are always different

                  Returns               Returns
                   Err[B]                Err[D]


exec(aa, bb, cc, dd)

      Returns A             Returns C
Exec type class
traitExec[In, Out] {
defapply(in: In): Out
}

object Exec {

implicit defcompExec[A] = newExec[Comp[A], A] {
defapply(in: Comp[A]): A = execute(in)
 }

implicit defioCompExec[A] = newExec[IOComp[A], Err[A]] {
defapply(in: IOComp[A]): Err[A] = ioExecute(in)
  }
}
Updated Runner type class
Runner return type is dependent on Exec return type

traitRunner[In, Out] {
defapply(in: In): Out
}

object Runner {
defexec[I,O](v: I)(implicitr: Runner[I,O]): O = r(v)

implicit def Tup1[T1](implicit ex1: Exec[T1,?]) =
new Runner[T1, ?] {
defapply(in: T1): ? = ex1(in)
  }                                               Needs to be
  ...                                           dependent on ex1
It gets worse

implicit def Tup2[T1,T2](implicit ex1: Exec[T1,?],
                      ex2: Exec[T2,?]) =
new Runner[(T1,T2),(?,?)] {
   defapply(in: (T1,T2)): (?,?) = (ex1(in._1),
                        ex2(in._2))
 }

 ...
Trick #3: Dependent types


Dependent types are types where the realization
  of the type created is actually dependent on
           the values being provided.
                                      (SCALABOUND)
Type parameters vs members
 traitExec[In, Out] {           Type
                             parameter
 defapply(in: In): Out
 }

          is equivalent to

 traitExec[In] {
 typeOut                      Type
                             member
 defapply(in: In): Out
 }
Exec using type members
traitExec[In] {
typeOut
defapply(in: In): Out
}

object Exec {

implicit defcompExec[A] = newExec[Comp[A]] {
typeOut = A
defapply(in: Comp[A]): A = execute(in)
  }

implicit defioCompExec[A] = newExec[IOComp[A]] {
typeOut = Err[A]
defapply(in: IOComp[A]): Err[A] = ioExecute(in)
  }
}
Using dependent method types
traitRunner[In] {
typeOut
defapply(in: In): Out                                         Return type is
                                                              dependent on
}
                                                             ‘r’. Access type
                                                              as a member.
object Runner {
defexec[R](v: R)(implicitr: Runner[R]): r.Out = r(v)

implicit def Tup1[T1](implicit ex1: Exec[T1]) =
new Runner[T1] {
typeOut = ex1.Out
defapply(in: T1): ex1.Out = ex1(in)
   }
                                                   Return type is dependent on
   ...                                                  ex1’s return type
And again
implicit def Tup2[T1,T2](implicit ex1: Exec[T1],
                      ex2: Exec[T2]) =
new Runner[(T1,T2)] {
typeOut = (ex1.Out, ex2.Out)
defapply(in: (T1,T2)): Out = (ex1(in._1),
                       ex2(in._2))
  }

 ...
Remember -Y
Dependent method types are still experimental!
        (but “turned-on” in 2.10.x)



        -Ydependent-method-types

API design: using type classes and dependent types

  • 1.
    API design: usingtype classes and dependent types Ben Lever @bmlever ScalaSyd Episode #6 11 July 2012
  • 2.
    What I want traitComp[A]{ ... } Specifies the “computation” of a value of type ‘A’ val ii: Comp[Int] = ... vali: Int = exec(ii) “Execute” the computation specified by ‘ii’.
  • 3.
    What would becool val ii: Comp[Int] = ... valss: Comp[String] = ... val (i: Int, s: String) = exec(ii, ss) “Execute” multiple computations of different types, then return “embedded” types as a tuple.
  • 4.
    More generally valaa: Comp[A]= ... val bb: Comp[B] = ... valcc: Comp[C] = ... valdd: Comp[D] = ... val (a: A, b: B, c: C, d: D) = exec(aa, bb, cc, dd) Ability to “execute” many computations simultaneously and return all “embedded” values together retaining types.
  • 5.
    But wait, there’smore traitIOComp[A] { ... } Specifies the “IO computation” of a value of type ‘A’ val ii: IOComp[Int] = ... Err[A] vali: Err[Int] = exec(ii) || Either[String, A]) Either return the “Execute” the IO computed value computation or an error string specified by ‘ii’.
  • 6.
    And of course valii: IOComp[Int] = ... valss: IOComp[String] = ... val (i: Err[Int], s: Err[String]) = exec(ii, ss) “Execute” multiple IO computations of different types, then return “embedded” types as a tuple.
  • 7.
    Finally valaa: Comp[A] =... val bb: IOComp[B] = ... valcc: Comp[C] = ... valdd: IOComp[D] = ... val (a: A, b: Err[B], c: C, d: Err[D]) = exec(aa, bb, cc, dd) “Execute” a mixture of normal and IO computations.
  • 8.
    Teaser - thefinal API defexec[R](v: R)(implicit runner: Runner[R]): runner.Out
  • 9.
    My inspiration “Couldn’t you use HLists/KLists for this?” Miles Sabin’s Shapeless provided much direction and inspirations But … I’m not going to talk about Shapeless 
  • 10.
    The tricks defexec[R](v: R)(implicitrunner: Runner[R]): runner.Out Tuple Type Dependent sweetness classes types
  • 11.
    Trick #1: tuplesweetness defecho[A](x: A) { prinln(x) } echo((“good”, 47, true)) echo(“good”, 47, true) // equivalent
  • 12.
    Trick #2: typeclasses Get the “size” of an object size(3) // 3 size(“hello”)// 5 size(false)// 1 ...
  • 13.
    Size type class traitSize[T]{ defapply(in: T): Int Type class } object Size { defsize[S](v: S)(implicits: Size[S]): Int = s(v) implicit defIntSize = newSize[Int] { defapply(in: Int): Int = in } implicit defBoolSize = newSize[Boolean] { defapply(in: Boolean): Int = 1 Type class instances } implicit defStringSize = newSize[String] { defapply(in: String): Int = in.length } }
  • 14.
    Under the hood Implicitobjects inserted by compiler size(3)(IntSize)// 3 size(“hello”)(StringSize)// 5 size(false)(BoolSize)// 1 ...
  • 15.
    Expanding the example Now,get the “size” of multiple objects at once size(3, “hello”,false)// (3,5,1)
  • 16.
    Runner type class traitRunner[In,Out] { defapply(in: In): Out } object Runner { defsize[I, O](v: I)(implicitr: Runner[I, O]): O = r(v) implicit def Tup1[T1](implicit s1: Size[T1]) = new Runner[T1, Int] { defapply(in: T1): Int = s1(in) Referencing } ‘Size’ implicit def Tup2[T1,T2](implicit s1: Size[T1], s2: Size[T2]) = new Runner[(T1,T2), (Int,Int)] { defapply(in: (T1,T2)): (Int,Int) = (s1(in._1), s2(in._2)) } implicit def Tup3[T1,T2,T3](implicit s1: Size[T1], ...
  • 17.
    Under the hood Implicitobject inserted by compiler size(3, “hello”,false) (Tup3 IntSize, StringSize, BoolSize))
  • 18.
    Aside: type classsugar defsize[S](v: S)(implicitsizer: Size[S]): Int is equivalent to defsize[S : Size](v: S) : Int Type class constraint
  • 19.
    ‘size’ is easy Runnerinstances know output types are always Ints Returns Int size(3, “hello”,false)// (3,5,1) Returns Int Returns Int
  • 20.
    ‘exec’ is harder Outputtypes are always different Returns Returns Err[B] Err[D] exec(aa, bb, cc, dd) Returns A Returns C
  • 21.
    Exec type class traitExec[In,Out] { defapply(in: In): Out } object Exec { implicit defcompExec[A] = newExec[Comp[A], A] { defapply(in: Comp[A]): A = execute(in) } implicit defioCompExec[A] = newExec[IOComp[A], Err[A]] { defapply(in: IOComp[A]): Err[A] = ioExecute(in) } }
  • 22.
    Updated Runner typeclass Runner return type is dependent on Exec return type traitRunner[In, Out] { defapply(in: In): Out } object Runner { defexec[I,O](v: I)(implicitr: Runner[I,O]): O = r(v) implicit def Tup1[T1](implicit ex1: Exec[T1,?]) = new Runner[T1, ?] { defapply(in: T1): ? = ex1(in) } Needs to be ... dependent on ex1
  • 23.
    It gets worse implicitdef Tup2[T1,T2](implicit ex1: Exec[T1,?], ex2: Exec[T2,?]) = new Runner[(T1,T2),(?,?)] { defapply(in: (T1,T2)): (?,?) = (ex1(in._1), ex2(in._2)) } ...
  • 24.
    Trick #3: Dependenttypes Dependent types are types where the realization of the type created is actually dependent on the values being provided. (SCALABOUND)
  • 25.
    Type parameters vsmembers traitExec[In, Out] { Type parameter defapply(in: In): Out } is equivalent to traitExec[In] { typeOut Type member defapply(in: In): Out }
  • 26.
    Exec using typemembers traitExec[In] { typeOut defapply(in: In): Out } object Exec { implicit defcompExec[A] = newExec[Comp[A]] { typeOut = A defapply(in: Comp[A]): A = execute(in) } implicit defioCompExec[A] = newExec[IOComp[A]] { typeOut = Err[A] defapply(in: IOComp[A]): Err[A] = ioExecute(in) } }
  • 27.
    Using dependent methodtypes traitRunner[In] { typeOut defapply(in: In): Out Return type is dependent on } ‘r’. Access type as a member. object Runner { defexec[R](v: R)(implicitr: Runner[R]): r.Out = r(v) implicit def Tup1[T1](implicit ex1: Exec[T1]) = new Runner[T1] { typeOut = ex1.Out defapply(in: T1): ex1.Out = ex1(in) } Return type is dependent on ... ex1’s return type
  • 28.
    And again implicit defTup2[T1,T2](implicit ex1: Exec[T1], ex2: Exec[T2]) = new Runner[(T1,T2)] { typeOut = (ex1.Out, ex2.Out) defapply(in: (T1,T2)): Out = (ex1(in._1), ex2(in._2)) } ...
  • 29.
    Remember -Y Dependent methodtypes are still experimental! (but “turned-on” in 2.10.x) -Ydependent-method-types