API design: using type classes and dependent types

  • 2,158 views
Uploaded on

A walkthrough of some of the techniques I used when implementing a particular API with challenging type requirements.

A walkthrough of some of the techniques I used when implementing a particular API with challenging type requirements.

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
No Downloads

Views

Total Views
2,158
On Slideshare
0
From Embeds
0
Number of Embeds
1

Actions

Shares
Downloads
0
Comments
2
Likes
5

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. API design: using type classes and dependent types Ben Lever @bmlever ScalaSyd Episode #6 11 July 2012
  • 2. What I wanttraitComp[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 be coolval 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 generallyvalaa: 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’s moretraitIOComp[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 courseval 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.
  • 7. Finallyvalaa: 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 - the final APIdefexec[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 inspirationsBut … I’m not going to talk about Shapeless 
  • 10. The tricksdefexec[R](v: R)(implicit runner: Runner[R]): runner.Out Tuple Type Dependent sweetness classes types
  • 11. Trick #1: tuple sweetnessdefecho[A](x: A) { prinln(x) }echo((“good”, 47, true))echo(“good”, 47, true) // equivalent
  • 12. Trick #2: type classesGet the “size” of an objectsize(3) // 3size(“hello”)// 5size(false)// 1...
  • 13. Size type classtraitSize[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 hoodImplicit objects inserted by compilersize(3)(IntSize)// 3size(“hello”)(StringSize)// 5size(false)(BoolSize)// 1...
  • 15. Expanding the exampleNow, get the “size” of multiple objects at oncesize(3, “hello”,false)// (3,5,1)
  • 16. Runner type classtraitRunner[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 hoodImplicit object inserted by compilersize(3, “hello”,false)(Tup3IntSize,StringSize,BoolSize))
  • 18. Aside: type class sugardefsize[S](v: S)(implicitsizer: Size[S]): Intis equivalent todefsize[S : Size](v: S) : Int Type class constraint
  • 19. ‘size’ is easyRunner instances know output types are always Ints Returns Intsize(3, “hello”,false)// (3,5,1) Returns Int Returns Int
  • 20. ‘exec’ is harderOutput types are always different Returns Returns Err[B] Err[D]exec(aa, bb, cc, dd) Returns A Returns C
  • 21. Exec type classtraitExec[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 type classRunner return type is dependent on Exec return typetraitRunner[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 worseimplicit 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)) } ...
  • 24. Trick #3: Dependent typesDependent types are types where the realization of the type created is actually dependent on the values being provided. (SCALABOUND)
  • 25. 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 }
  • 26. Exec using type memberstraitExec[In] {typeOutdefapply(in: In): Out}object Exec {implicit defcompExec[A] = newExec[Comp[A]] {typeOut = Adefapply(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 method typestraitRunner[In] {typeOutdefapply(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.Outdefapply(in: T1): ex1.Out = ex1(in) } Return type is dependent on ... ex1’s return type
  • 28. And againimplicit 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)) } ...
  • 29. Remember -YDependent method types are still experimental! (but “turned-on” in 2.10.x) -Ydependent-method-types