diff --git a/sofp-src/sofp-draft.pdf b/sofp-src/sofp-draft.pdf index 66f6807c9..702afe0bf 100644 Binary files a/sofp-src/sofp-draft.pdf and b/sofp-src/sofp-draft.pdf differ diff --git a/sofp-src/sofp-typeclasses.lyx b/sofp-src/sofp-typeclasses.lyx index f4da392be..6d7e5787c 100644 --- a/sofp-src/sofp-typeclasses.lyx +++ b/sofp-src/sofp-typeclasses.lyx @@ -32589,8 +32589,7 @@ kind projector \begin_inset Quotes erd \end_inset - plugin is often necessary when writing code involving higher-order type - functions: + plugin is often needed when writing code with higher-order type functions: \begin_inset listings lstparams "mathescape=true" inline false @@ -32634,7 +32633,7 @@ rightarrow *$ \begin_layout Plain Layout -type X2 = Twice[Ap, O2, Int] // X2 is now Option[(Option[(Int, Int)], +type X2 = Twice[Ap1, O2, Int] // X2 is now Option[(Option[(Int, Int)], Option[(Int, Int)])] \end_layout @@ -34064,12 +34063,12 @@ func[A: Monoid] \end_inset -, restricts type parameters to a certain type domain. +, restricts a type parameter to a certain type domain. Sometimes it is necessary to restrict \emph on several \emph default - type parameters to satisfy a given relation. + type parameters to satisfy some conditions together. Let us look at two simple examples of this. \end_layout @@ -34519,7 +34518,7 @@ implicit val ev1 = MayConvert[Short, Float](_.toFloat) // Evidence \begin_layout Plain Layout -implicit val ev2 = MayConvert[Int, Double](_.toDouble) // Evidence +implicit val ev2 = MayConvert[Int, Double](_.toDouble) // Evidence value for [Int, Double]. \end_layout @@ -34599,6 +34598,798 @@ String,N] \end_inset +\end_layout + +\begin_layout Standard +As we have just seen, a type relation is defined by creating a set of evidence + values. + The code defines +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +MayConvert +\end_layout + +\end_inset + + as a +\begin_inset Formula $1$ +\end_inset + +-to- +\begin_inset Formula $1$ +\end_inset + + relation because the evidence values (or +\begin_inset Quotes eld +\end_inset + +relation instances +\begin_inset Quotes erd +\end_inset + +) +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +ev1 +\end_layout + +\end_inset + + and +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +ev2 +\end_layout + +\end_inset + + do not have any type parameters in common. + So, +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +MayConvert +\end_layout + +\end_inset + + is equivalent to a type-to-type +\emph on +function +\emph default + that maps +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Short +\end_layout + +\end_inset + + to +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Float +\end_layout + +\end_inset + + and +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Int +\end_layout + +\end_inset + + to +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Double +\end_layout + +\end_inset + +. + However, type relations are not limited to +\begin_inset Formula $1$ +\end_inset + +-to- +\begin_inset Formula $1$ +\end_inset + + relations or to type functions. + By creating suitable implicit evidence values, we can implement +\begin_inset Formula $1$ +\end_inset + +-to-many or many-to-many relations when needed. + +\end_layout + +\begin_layout Standard +A practical example of a many-to-many type relation is seen in conversion + rules between physical units. + Miles can be converted to kilometers, while pounds can be converted to + ounces or kilograms, but kilograms cannot be converted into kilometers. + Type relations allow us to implement type-safe operations for quantities + with units. + Adding kilograms to pounds will automatically convert the quantity to a + common unit, while adding kilograms to kilometers will raise a compile-time + error. +\end_layout + +\begin_layout Standard +Begin by declaring a type for +\begin_inset Quotes eld +\end_inset + +quantity with units +\begin_inset Quotes erd +\end_inset + + and some type names for the supported units: +\begin_inset Index idx +status open + +\begin_layout Plain Layout +constant functor +\end_layout + +\end_inset + + +\begin_inset listings +inline false +status open + +\begin_layout Plain Layout + +trait KG; trait LB; trait OZ; trait KM; trait MI; trait FT +\end_layout + +\begin_layout Plain Layout + +final case class Quantity[U](value: Double) // Constant functor: No values + of type U are stored. +\end_layout + +\begin_layout Plain Layout + +def add[U1, U2](x: Quantity[U1], y: Quantity[U2]): Quantity[U2] = ??? +\end_layout + +\end_inset + +The +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +add +\end_layout + +\end_inset + + function must have a type relation constraint for its type parameters +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +U +\end_layout + +\end_inset + + and +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +U2 +\end_layout + +\end_inset + +, so that only quantities with compatible units may be added. + To implement that, we define a type constructor with two type parameters. + A relation instance will contain a multiplier for converting the units: +\begin_inset listings +inline false +status open + +\begin_layout Plain Layout + +final case class Convertible[U, U2](multiplier: Double) +\end_layout + +\begin_layout Plain Layout + +implicit val c1 = Convertible[KG, KG](1.0) +\end_layout + +\begin_layout Plain Layout + +implicit val c2 = Convertible[LB, KG](0.453592) +\end_layout + +\begin_layout Plain Layout + +implicit val c3 = Convertible[KM, KM](1.0) +\end_layout + +\begin_layout Plain Layout + +implicit val c4 = Convertible[MI, KM](1.60934) +\end_layout + +\begin_layout Plain Layout + +// Add many more definitions here... +\end_layout + +\end_inset + +Now we can implement the +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +add +\end_layout + +\end_inset + + function and verify that the type relation works as we intended: +\begin_inset listings +inline false +status open + +\begin_layout Plain Layout + +def add[U1, U2](x: Quantity[U1], y: Quantity[U2])(implicit ev: Convertible[U1, + U2]): Quantity[U2] = +\end_layout + +\begin_layout Plain Layout + + Quantity(x.value * ev.multiplier + y.value) +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +scala> add(Quantity[LB](1), Quantity[KG](1)) // 1 pound + 1 kg = 1.453592 + kg +\end_layout + +\begin_layout Plain Layout + +res0: Quantity[KG] = Quantity(1.453592) +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +scala> add(Quantity[MI](1), Quantity[KG](1)) // Compile-time error: cannot + add miles to kilograms. +\end_layout + +\begin_layout Plain Layout + +:25: error: could not find implicit value for parameter ev: Convertible +[MI,KG] +\end_layout + +\begin_layout Plain Layout + + add(Quantity[MI](1), Quantity[KG](1)) +\end_layout + +\begin_layout Plain Layout + + ^ +\end_layout + +\end_inset + + +\end_layout + +\begin_layout Standard +To make this code more convenient for practical use, we can shorten the + syntax from +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Quantity[KG](1) +\end_layout + +\end_inset + + to +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +1.kg +\end_layout + +\end_inset + + and from +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +add(2.lb, 2.kg) +\end_layout + +\end_inset + + to a more readable +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +2.lb + 2.kg +\end_layout + +\end_inset + + by adding extension methods: +\begin_inset listings +inline false +status open + +\begin_layout Plain Layout + +implicit class QuantitySyntax(x: Double) { +\end_layout + +\begin_layout Plain Layout + + def kg = Quantity[KG](x) +\end_layout + +\begin_layout Plain Layout + + def lb = Quantity[LB](x) +\end_layout + +\begin_layout Plain Layout + +} +\end_layout + +\begin_layout Plain Layout + +implicit class QuantityAdd[U1](x: Quantity[U1]) { +\end_layout + +\begin_layout Plain Layout + + def +[U2](y: Quantity[U2])(implicit ev: Convertible[U1, U2]): Quantity[U2] + = add(x, y) +\end_layout + +\begin_layout Plain Layout + +} +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +scala> 2.lb + 2.kg +\end_layout + +\begin_layout Plain Layout + +res1: Quantity[KG] = Quantity(2.907184) +\end_layout + +\end_inset + + +\end_layout + +\begin_layout Standard +Another necessary improvement is reducing the number of implicit values. + The current code uses +\begin_inset Formula $n^{2}$ +\end_inset + + implicit values for every group of +\begin_inset Formula $n$ +\end_inset + + compatible units. + Adding support for a new unit (say, inches) requires adding implicit values + for converting between inches and all previously defined units of length. + To avoid this problem, the code must be reorganized to convert all compatible + quantities to chosen units (e.g. +\begin_inset space ~ +\end_inset + +all length to kilometers and all mass to kilograms). + This makes the +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Convertible +\end_layout + +\end_inset + + relation many-to- +\begin_inset Formula $1$ +\end_inset + + instead of many-to-many. + The resulting code is shown in Figure +\begin_inset space ~ +\end_inset + + +\begin_inset CommandInset ref +LatexCommand ref +reference "fig:Full-code-implementing-units-length-mass" +plural "false" +caps "false" +noprefix "false" + +\end_inset + +. +\end_layout + +\begin_layout Standard +\begin_inset Float figure +wide false +sideways false +status open + +\begin_layout Plain Layout +\begin_inset listings +lstparams "frame=single,fillcolor={\color{black}},framesep={0.2mm},framexleftmargin=2mm,framexrightmargin=2mm,framextopmargin=2mm,framexbottommargin=2mm" +inline false +status open + +\begin_layout Plain Layout + +trait KG; trait LB; trait OZ; trait KM; trait MI; trait FT +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +// This type relation is many-to-1: it relates all mass units to KG and + all length units to KM. +\end_layout + +\begin_layout Plain Layout + +final case class Convertible[U1, U2](multiplier: Double) extends AnyVal +\end_layout + +\begin_layout Plain Layout + +implicit val cKG = Convertible[KG, KG](1.0) +\end_layout + +\begin_layout Plain Layout + +implicit val cLB = Convertible[LB, KG](0.453592) +\end_layout + +\begin_layout Plain Layout + +implicit val cOZ = Convertible[OZ, KG](0.0283495) +\end_layout + +\begin_layout Plain Layout + +implicit val cKM = Convertible[KM, KM](1.0) +\end_layout + +\begin_layout Plain Layout + +implicit val cMI = Convertible[MI, KM](1.60934) +\end_layout + +\begin_layout Plain Layout + +implicit val cFT = Convertible[FT, KM](0.0003048) +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +final case class Quantity[U](value: Double) extends AnyVal { // Use `AnyVal` + to reduce run-time cost. +\end_layout + +\begin_layout Plain Layout + + def +[U2, C](q: Quantity[U2])(implicit ev1: Convertible[U, C], ev2: Convertibl +e[U2, C]) = +\end_layout + +\begin_layout Plain Layout + + Quantity[U2](value * ev1.multiplier / ev2.multiplier + q.value) +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + + def ==[U2, C](q: Quantity[U2])(implicit ev1: Convertible[U, C], ev2: Convertib +le[U2, C]) = +\end_layout + +\begin_layout Plain Layout + + value * ev1.multiplier == q.value * ev2.multiplier +\end_layout + +\begin_layout Plain Layout + +} +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +implicit class QuantitySyntax(x: Double) { +\end_layout + +\begin_layout Plain Layout + + def kg = Quantity[KG](x) +\end_layout + +\begin_layout Plain Layout + + def lb = Quantity[LB](x) +\end_layout + +\begin_layout Plain Layout + + def oz = Quantity[OZ](x) +\end_layout + +\begin_layout Plain Layout + + def km = Quantity[KM](x) +\end_layout + +\begin_layout Plain Layout + + def mi = Quantity[MI](x) +\end_layout + +\begin_layout Plain Layout + + def ft = Quantity[FT](x) +\end_layout + +\begin_layout Plain Layout + + // A general extension method, e.g. + `1.in[KM]`, that will also work for any units defined later. +\end_layout + +\begin_layout Plain Layout + + def in[U](implicit ev: Convertible[U, _]): Quantity[U] = Quantity[U](x) +\end_layout + +\begin_layout Plain Layout + +} +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +scala> 1.in[KM] // Use the general method `.in` with a type parameter. +\end_layout + +\begin_layout Plain Layout + +res1: Quantity[KM] = Quantity(1.0) +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +scala> 10000.ft + 1.km // Use the `.ft` and `.km` methods. +\end_layout + +\begin_layout Plain Layout + +res2: Quantity[KM] = Quantity(4.048) +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +scala> 1.kg + 1.lb == 16.oz + 1.kg // Compare values safely. +\end_layout + +\begin_layout Plain Layout + +res2: Boolean = true +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +scala> 1.km + 1.lb // Compile-time error: cannot add miles to kilograms. +\end_layout + +\begin_layout Plain Layout + +:29: error: could not find implicit value for parameter ev2: Convertibl +e[LB,C] +\end_layout + +\begin_layout Plain Layout + + 1.km + 1.lb +\end_layout + +\begin_layout Plain Layout + + ^ +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +scala> trait YD; implicit val cYD = Convertible[YD, KM](0.0009144) // Easily + add support for yards. +\end_layout + +\begin_layout Plain Layout + +defined trait YD +\end_layout + +\begin_layout Plain Layout + +cIN: Convertible[YD,KM] = Convertible(9.144E-4) +\end_layout + +\begin_layout Plain Layout + +\end_layout + +\begin_layout Plain Layout + +scala> 1.in[YD] + 1.ft // Use .in[YD] to compute 1 YD + 1 FT = 4 FT. +\end_layout + +\begin_layout Plain Layout + +res3: Quantity[FT] = Quantity(4.0) +\end_layout + +\end_inset + + +\end_layout + +\begin_layout Plain Layout +\begin_inset Caption Standard + +\begin_layout Plain Layout +Implementing type-safe computations with units of length and mass. +\begin_inset CommandInset label +LatexCommand label +name "fig:Full-code-implementing-units-length-mass" + +\end_inset + + +\end_layout + +\end_inset + + +\end_layout + +\end_inset + + \end_layout \begin_layout Subsection @@ -34613,8 +35404,719 @@ name "subsec:Inheritance-and-automatic-typeclass" \end_layout \begin_layout Standard -There exists a Scala encoding of typeclasses that solves the problem of - duplicate inherited instances. +It often happens that one typeclass implies another; for example, a +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Monoid +\end_layout + +\end_inset + + instance for a type +\begin_inset Formula $T$ +\end_inset + + means that +\begin_inset Formula $T$ +\end_inset + + already has the properties of both +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Semigroup +\end_layout + +\end_inset + + and +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +HasDefault +\end_layout + +\end_inset + + typeclasses. + One says that the +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Monoid +\end_layout + +\end_inset + + typeclass +\series bold +inherits +\series default + +\begin_inset Index idx +status open + +\begin_layout Plain Layout +typeclass!inheritance +\end_layout + +\end_inset + + from the +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Semigroup +\end_layout + +\end_inset + + and +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +HasDefault +\end_layout + +\end_inset + + typeclasses. + We may want to express inheritance relations between typeclasses, so that + e.g. +\begin_inset space ~ +\end_inset + +a +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Monoid +\end_layout + +\end_inset + + instance should automatically imply the presence of a +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Semigroup +\end_layout + +\end_inset + + instance without extra code. +\end_layout + +\begin_layout Standard +One way of inheriting a typeclass is to add a constraint to the new typeclass + constructor: +\begin_inset listings +inline false +status open + +\begin_layout Plain Layout + +final case class Semigroup[T](combine: (T, T) => T) +\end_layout + +\begin_layout Plain Layout + +final case class Monoid[T: Semigroup](empty: T) // Inherit `Semigroup`. +\end_layout + +\end_inset + +Creating an instance of +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Monoid +\end_layout + +\end_inset + + for a type +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +T +\end_layout + +\end_inset + + will then require having an instance of +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Semigroup +\end_layout + +\end_inset + + for +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +T +\end_layout + +\end_inset + +: +\begin_inset listings +inline false +status open + +\begin_layout Plain Layout + +implicit val c1 = Semigroup[Int](_ + _) +\end_layout + +\begin_layout Plain Layout + +implicit val c2 = Monoid[Int](0) // Works only if a `Semigroup[Int]` is + available. +\end_layout + +\end_inset + + +\end_layout + +\begin_layout Standard +If we now have a +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Monoid +\end_layout + +\end_inset + + instance, how can we recover the inherited +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Semigroup +\end_layout + +\end_inset + + instance? We need to extract the implicit value of type +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Semigroup[T] +\end_layout + +\end_inset + + that was passed to the +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Monoid +\end_layout + +\end_inset + + class constructor. + This is achieved by adding an implicit function and by redefining the +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Monoid +\end_layout + +\end_inset + + class as follows, +\begin_inset listings +inline false +status open + +\begin_layout Plain Layout + +final case class Monoid[T](empty: T)(implicit val semigroup: Semigroup[T]) + // Use `implicit val`. +\end_layout + +\begin_layout Plain Layout + +implicit def semigroupFromMonoid[T](implicit ti: Monoid[T]): Semigroup[T] + = ti.semigroup +\end_layout + +\end_inset + +The case class +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Monoid +\end_layout + +\end_inset + + combines a previous +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Semigroup +\end_layout + +\end_inset + + instance with the new method +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +empty +\end_layout + +\end_inset + +. +\end_layout + +\begin_layout Standard +Another possibility of implementing typeclass inheritance is to use object-orien +ted inheritance of classes or traits. + This approach is often used when defining typeclasses as traits with methods: +\begin_inset listings +inline false +status open + +\begin_layout Plain Layout + +trait Semigroup[T] { def combine: (T, T) => T } +\end_layout + +\begin_layout Plain Layout + +trait Monoid[T] extends Semigroup[T] { def empty: T } // `def combine` is + inherited from `Semigroup`. +\end_layout + +\end_inset + +Creating an instance of +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Monoid +\end_layout + +\end_inset + + for a type +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +T +\end_layout + +\end_inset + + no longer requires having an instance of +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Semigroup +\end_layout + +\end_inset + + for +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +T +\end_layout + +\end_inset + +: +\end_layout + +\begin_layout Standard +\begin_inset Wrap figure +lines 0 +placement l +overhang 0in +width "43col%" +status open + +\begin_layout Plain Layout +\begin_inset VSpace -85baselineskip% +\end_inset + + +\begin_inset listings +inline false +status open + +\begin_layout Plain Layout + +implicit val c: Monoid[Int] = new Monoid[Int] { +\end_layout + +\begin_layout Plain Layout + + def empty: Int = 0 +\end_layout + +\begin_layout Plain Layout + + def combine: (Int, Int) => Int = _ + _ +\end_layout + +\begin_layout Plain Layout + +} +\end_layout + +\end_inset + + +\end_layout + +\begin_layout Plain Layout +\begin_inset VSpace -100baselineskip% +\end_inset + + +\end_layout + +\end_inset + + +\end_layout + +\begin_layout Standard +\noindent +With this approach, we cannot avoid repeating the code for +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +def combine... +\end_layout + +\end_inset + + even if we +\emph on +do +\emph default + already have a +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Semigroup +\end_layout + +\end_inset + + instance. + However, conversion from +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Monoid[T] +\end_layout + +\end_inset + + to +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Semigroup[T] +\end_layout + +\end_inset + + is now automatic due to +\series bold +object-oriented inheritance +\series default + +\begin_inset Index idx +status open + +\begin_layout Plain Layout +object-oriented inheritance +\end_layout + +\end_inset + +: +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Monoid[T] +\end_layout + +\end_inset + + is a subtype of +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +Semigroup[T] +\end_layout + +\end_inset + + because the +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout +\noindent + +Monoid +\end_layout + +\end_inset + + class is declared as +\begin_inset listings +inline true +status open + +\begin_layout Plain Layout + +extends Semigroup +\end_layout + +\end_inset + +. +\end_layout + +\begin_layout Standard +One problem with the object-oriented inheritance is that automatic conversions + to parent typeclasses cannot be disabled. + When several typeclasses inherit from the same parent typeclass, duplicate + implicit instances of the parent typeclass may be present. + This will give a compile-time error: +\begin_inset listings +inline false +status open + +\begin_layout Plain Layout + +trait TC[A] // Parent typeclass. +\end_layout + +\begin_layout Plain Layout + +trait TC1[A] extends TC[A] // Two different typeclasses TC1 and TC2 + inherit from TC. +\end_layout + +\begin_layout Plain Layout + +trait TC2[A] extends TC[A] +\end_layout + +\begin_layout Plain Layout + +// The function f requires A to have both TC1 and TC2 instances and then + wants to access TC instance. +\end_layout + +\begin_layout Plain Layout + +def f[A: TC1 : TC2]() = { +\end_layout + +\begin_layout Plain Layout + + implicitly[TC[A]] // Compilation fails because two implicits of type + TC[A] are found. +\end_layout + +\begin_layout Plain Layout + +} +\end_layout + +\end_inset + + +\end_layout + +\begin_layout Standard +When typeclass inheritance is implemented by combining instances, conversions + to parent typeclasses are not automatic – they are implemented by implicit + functions that need to be explicitly imported into the current scope. + The programmer's code can avoid producing duplicate instances by choosing + which implicit conversions to use: +\begin_inset listings +inline false +status open + +\begin_layout Plain Layout + +final case class TC[A]() +\end_layout + +\begin_layout Plain Layout + +final case class TC1[A]()(implicit val tc0: TC[A]) +\end_layout + +\begin_layout Plain Layout + +object TC1 { implicit def toTC[A](implicit x: TC1[A]): TC[A] = x.tc0 } +\end_layout + +\begin_layout Plain Layout + +final case class TC2[A]()(implicit val tc0: TC[A]) +\end_layout + +\begin_layout Plain Layout + +object TC2 { implicit def toTC[A](implicit x: TC2[A]): TC[A] = x.tc0 } +\end_layout + +\begin_layout Plain Layout + +// The function f requires A to have both TC1 and TC2 instances and then + wants to access TC instance. +\end_layout + +\begin_layout Plain Layout + +def f[A: TC1 : TC2]() = { +\end_layout + +\begin_layout Plain Layout + + import TC1._ // Can import TC1._ or TC2._ but not both. + If the next line is uncommented, +\end_layout + +\begin_layout Plain Layout + +// import TC2._ // compilation will fail because two implicits of type + TC[A] will be found! +\end_layout + +\begin_layout Plain Layout + + implicitly[TC[A]] // This compiles successfully. + One implicit instance of TC[A] can be found. +\end_layout + +\begin_layout Plain Layout + +} +\end_layout + +\end_inset + + +\end_layout + +\begin_layout Standard +The problem of duplicate inherited instances can be solved with less work + for the programmer at the cost of a more complicated encoding of typeclasses. \begin_inset Foot status open diff --git a/sofp-src/sofp-typeclasses.tex b/sofp-src/sofp-typeclasses.tex index db3bcd191..612f3efdb 100644 --- a/sofp-src/sofp-typeclasses.tex +++ b/sofp-src/sofp-typeclasses.tex @@ -5184,13 +5184,13 @@ \subsection{``Kinds'' and higher-order type functions} type Y = Ap2[Ap, Ap, Int] // Type error: the second argument of Ap2 has wrong kind. type Z = Ap2[G, G, Int] // Type error: the first argument of Ap2 has wrong kind. \end{lstlisting} -The ``kind projector'' plugin is often necessary when writing code -involving higher-order type functions: +The ``kind projector'' plugin is often needed when writing code +with higher-order type functions: \begin{lstlisting}[mathescape=true] // `Twice` will apply the type constructor Q twice to its type argument, and substitute into P. type Twice[P[_[_], _], Q[_], R] = P[Lambda[X => Q[Q[X]]], R] // $\text{Twice}: (((*\rightarrow*)\times*\rightarrow*)\times(*\rightarrow*)\times*)\rightarrow* $ type O2[A] = Option[(A, A)] // $\text{O2}: * \rightarrow *$ -type X2 = Twice[Ap, O2, Int] // X2 is now Option[(Option[(Int, Int)], Option[(Int, Int)])] +type X2 = Twice[Ap1, O2, Int] // X2 is now Option[(Option[(Int, Int)], Option[(Int, Int)])] val x2: X2 = Some((Some((1, 2)), Some((3, 4)))) // Types match for `x2`. \end{lstlisting} @@ -5387,10 +5387,319 @@ \subsection{Inductive typeclasses and their properties} \subsection{Typeclasses with more than one type parameter (type relations)} +A typeclass constraint in a function, such as \lstinline!func[A: Monoid]!, +restricts a type parameter to a certain type domain. Sometimes it +is necessary to restrict \emph{several} type parameters to satisfy +some conditions together. Let us look at two simple examples of this. + +The first example is converting integer numbers to floating point. +The ranges of the available types allow us to convert a \lstinline!Short! +to a \lstinline!Float! and an \lstinline!Int! to a \lstinline!Double!. +Can we implement the type signature +\begin{lstlisting} +def convertNumber[M, N](x: M): N +\end{lstlisting} +where the type parameters \lstinline!M!, \lstinline!N! are constrained +to be either \lstinline!M = Short! and \lstinline!N = Float!, or +\lstinline!M = Int! and \lstinline!N = Double!? (Of course, we will +want to be able to add further supported pairs of types to this code.) + +A condition that constrains both type parameters at once is called +a \index{type relation}\textbf{type relation}. + +The second example is converting mutable data structures into the +corresponding immutable ones. The Scala library supports data structures +such as sequences, sets, and dictionaries, each having a mutable and +an immutable version. Can we implement a function with type signature +\begin{lstlisting} +def convertData[Mut[_], Immut[_], A](data: Mut[A]): Immut[A] +\end{lstlisting} +where the type parameters \lstinline!Mut! and \lstinline!Immut! +are constrained to represent the mutable / immutable versions of a +supported data structure (for example, \lstinline!Mut = mutable.Set! +and \lstinline!Immut = immutable.Set!, etc.)? + +As we have seen, an ordinary typeclass constraint (for a single type +parameter), such as \lstinline!func[A: Monoid]!, is implemented by +requiring an ``evidence'' value of type \lstinline!Monoid[A]! as +an additional argument of a function \lstinline!func!. Implementing +a type relation for \emph{two} type parameters \lstinline!A!, \lstinline!B! +is similar: We first define a type constructor, say \lstinline!Rel[A, B]!, +and create some evidence values of that type, with specific chosen +type parameters \lstinline!A!, \lstinline!B!. Any function that +requires the relation constraint on its type parameters \lstinline!A!, +\lstinline!B! will receive an implicit ``evidence argument'' of +type \lstinline!Rel[A, B]!. This will prevent using that function +for types \lstinline!A!, \lstinline!B! that are not in the required +relation. + +The code for \lstinline!convertNumber! can be written like this: +\begin{lstlisting} +final case class MayConvert[A, B](convert: A => B) // Evidence value contains a conversion function. +implicit val ev1 = MayConvert[Short, Float](_.toFloat) // Evidence value for [Short, Float]. +implicit val ev2 = MayConvert[Int, Double](_.toDouble) // Evidence value for [Int, Double]. +def convertNumber[M, N](x: M)(implicit ev: MayConvert[M, N]): N = ev.convert(x) +\end{lstlisting} +With these definitions, it will be a compile-time error to use \lstinline!convertNumber! +on unsupported types: +\begin{lstlisting} +scala> convertNumber(123) +res0: Double = 123.0 + +scala> convertNumber(123:Short) +res1: Float = 123.0 + +scala> convertNumber("abc") +:17: error: could not find implicit value for parameter ev: MayConvert[String,N] + convertNumber("abc") + ^ +\end{lstlisting} + +As we have just seen, a type relation is defined by creating a set +of evidence values. The code defines \lstinline!MayConvert! as a +$1$-to-$1$ relation because the evidence values (or ``relation +instances'') \lstinline!ev1! and \lstinline!ev2! do not have any +type parameters in common. So, \lstinline!MayConvert! is equivalent +to a type-to-type \emph{function} that maps \lstinline!Short! to +\lstinline!Float! and \lstinline!Int! to \lstinline!Double!. However, +type relations are not limited to $1$-to-$1$ relations or to type +functions. By creating suitable implicit evidence values, we can implement +$1$-to-many or many-to-many relations when needed. + +A practical example of a many-to-many type relation is seen in conversion +rules between physical units. Miles can be converted to kilometers, +while pounds can be converted to ounces or kilograms, but kilograms +cannot be converted into kilometers. Type relations allow us to implement +type-safe operations for quantities with units. Adding kilograms to +pounds will automatically convert the quantity to a common unit, while +adding kilograms to kilometers will raise a compile-time error. + +Begin by declaring a type for ``quantity with units'' and some type +names for the supported units:\index{constant functor} +\begin{lstlisting} +trait KG; trait LB; trait OZ; trait KM; trait MI; trait FT +final case class Quantity[U](value: Double) // Constant functor: No values of type U are stored. +def add[U1, U2](x: Quantity[U1], y: Quantity[U2]): Quantity[U2] = ??? +\end{lstlisting} +The \lstinline!add! function must have a type relation constraint +for its type parameters \lstinline!U! and \lstinline!U2!, so that +only quantities with compatible units may be added. To implement that, +we define a type constructor with two type parameters. A relation +instance will contain a multiplier for converting the units: +\begin{lstlisting} +final case class Convertible[U, U2](multiplier: Double) +implicit val c1 = Convertible[KG, KG](1.0) +implicit val c2 = Convertible[LB, KG](0.453592) +implicit val c3 = Convertible[KM, KM](1.0) +implicit val c4 = Convertible[MI, KM](1.60934) +// Add many more definitions here... +\end{lstlisting} +Now we can implement the \lstinline!add! function and verify that +the type relation works as we intended: +\begin{lstlisting} +def add[U1, U2](x: Quantity[U1], y: Quantity[U2])(implicit ev: Convertible[U1, U2]): Quantity[U2] = + Quantity(x.value * ev.multiplier + y.value) + +scala> add(Quantity[LB](1), Quantity[KG](1)) // 1 pound + 1 kg = 1.453592 kg +res0: Quantity[KG] = Quantity(1.453592) + +scala> add(Quantity[MI](1), Quantity[KG](1)) // Compile-time error: cannot add miles to kilograms. +:25: error: could not find implicit value for parameter ev: Convertible[MI,KG] + add(Quantity[MI](1), Quantity[KG](1)) + ^ +\end{lstlisting} + +To make this code more convenient for practical use, we can shorten +the syntax from \lstinline!Quantity[KG](1)! to \lstinline!1.kg! +and from \lstinline!add(2.lb, 2.kg)! to a more readable \lstinline!2.lb + 2.kg! +by adding extension methods: +\begin{lstlisting} +implicit class QuantitySyntax(x: Double) { + def kg = Quantity[KG](x) + def lb = Quantity[LB](x) +} +implicit class QuantityAdd[U1](x: Quantity[U1]) { + def +[U2](y: Quantity[U2])(implicit ev: Convertible[U1, U2]): Quantity[U2] = add(x, y) +} + +scala> 2.lb + 2.kg +res1: Quantity[KG] = Quantity(2.907184) +\end{lstlisting} + +Another necessary improvement is reducing the number of implicit values. +The current code uses $n^{2}$ implicit values for every group of +$n$ compatible units. Adding support for a new unit (say, inches) +requires adding implicit values for converting between inches and +all previously defined units of length. To avoid this problem, the +code must be reorganized to convert all compatible quantities to chosen +units (e.g.~all length to kilometers and all mass to kilograms). +This makes the \lstinline!Convertible! relation many-to-$1$ instead +of many-to-many. The resulting code is shown in Figure~\ref{fig:Full-code-implementing-units-length-mass}. + +\begin{figure} +\begin{lstlisting}[frame=single,fillcolor={\color{black}},framesep={0.2mm},framexleftmargin=2mm,framexrightmargin=2mm,framextopmargin=2mm,framexbottommargin=2mm] +trait KG; trait LB; trait OZ; trait KM; trait MI; trait FT + +// This type relation is many-to-1: it relates all mass units to KG and all length units to KM. +final case class Convertible[U1, U2](multiplier: Double) extends AnyVal +implicit val cKG = Convertible[KG, KG](1.0) +implicit val cLB = Convertible[LB, KG](0.453592) +implicit val cOZ = Convertible[OZ, KG](0.0283495) +implicit val cKM = Convertible[KM, KM](1.0) +implicit val cMI = Convertible[MI, KM](1.60934) +implicit val cFT = Convertible[FT, KM](0.0003048) + +final case class Quantity[U](value: Double) extends AnyVal { // Use `AnyVal` to reduce run-time cost. + def +[U2, C](q: Quantity[U2])(implicit ev1: Convertible[U, C], ev2: Convertible[U2, C]) = + Quantity[U2](value * ev1.multiplier / ev2.multiplier + q.value) + + def ==[U2, C](q: Quantity[U2])(implicit ev1: Convertible[U, C], ev2: Convertible[U2, C]) = + value * ev1.multiplier == q.value * ev2.multiplier +} + +implicit class QuantitySyntax(x: Double) { + def kg = Quantity[KG](x) + def lb = Quantity[LB](x) + def oz = Quantity[OZ](x) + def km = Quantity[KM](x) + def mi = Quantity[MI](x) + def ft = Quantity[FT](x) + // A general extension method, e.g. `1.in[KM]`, that will also work for any units defined later. + def in[U](implicit ev: Convertible[U, _]): Quantity[U] = Quantity[U](x) +} + +scala> 1.in[KM] // Use the general method `.in` with a type parameter. +res1: Quantity[KM] = Quantity(1.0) + +scala> 10000.ft + 1.km // Use the `.ft` and `.km` methods. +res2: Quantity[KM] = Quantity(4.048) + +scala> 1.kg + 1.lb == 16.oz + 1.kg // Compare values safely. +res2: Boolean = true + +scala> 1.km + 1.lb // Compile-time error: cannot add miles to kilograms. +:29: error: could not find implicit value for parameter ev2: Convertible[LB,C] + 1.km + 1.lb + ^ + +scala> trait YD; implicit val cYD = Convertible[YD, KM](0.0009144) // Easily add support for yards. +defined trait YD +cIN: Convertible[YD,KM] = Convertible(9.144E-4) + +scala> 1.in[YD] + 1.ft // Use .in[YD] to compute 1 YD + 1 FT = 4 FT. +res3: Quantity[FT] = Quantity(4.0) +\end{lstlisting} + +\caption{Implementing type-safe computations with units of length and mass.\label{fig:Full-code-implementing-units-length-mass}} +\end{figure} + + \subsection{Inheritance and automatic conversions of typeclasses\label{subsec:Inheritance-and-automatic-typeclass}} -There exists a Scala encoding of typeclasses that solves the problem -of duplicate inherited instances.\footnote{See slides 5\textendash 14 of this talk by \index{John de Goes}John +It often happens that one typeclass implies another; for example, +a \lstinline!Monoid! instance for a type $T$ means that $T$ already +has the properties of both \lstinline!Semigroup! and \lstinline!HasDefault! +typeclasses. One says that the \lstinline!Monoid! typeclass \textbf{inherits}\index{typeclass!inheritance} +from the \lstinline!Semigroup! and \lstinline!HasDefault! typeclasses. +We may want to express inheritance relations between typeclasses, +so that e.g.~a \lstinline!Monoid! instance should automatically +imply the presence of a \lstinline!Semigroup! instance without extra +code. + +One way of inheriting a typeclass is to add a constraint to the new +typeclass constructor: +\begin{lstlisting} +final case class Semigroup[T](combine: (T, T) => T) +final case class Monoid[T: Semigroup](empty: T) // Inherit `Semigroup`. +\end{lstlisting} +Creating an instance of \lstinline!Monoid! for a type \lstinline!T! +will then require having an instance of \lstinline!Semigroup! for +\lstinline!T!: +\begin{lstlisting} +implicit val c1 = Semigroup[Int](_ + _) +implicit val c2 = Monoid[Int](0) // Works only if a `Semigroup[Int]` is available. +\end{lstlisting} + +If we now have a \lstinline!Monoid! instance, how can we recover +the inherited \lstinline!Semigroup! instance? We need to extract +the implicit value of type \lstinline!Semigroup[T]! that was passed +to the \lstinline!Monoid! class constructor. This is achieved by +adding an implicit function and by redefining the \lstinline!Monoid! +class as follows, +\begin{lstlisting} +final case class Monoid[T](empty: T)(implicit val semigroup: Semigroup[T]) // Use `implicit val`. +implicit def semigroupFromMonoid[T](implicit ti: Monoid[T]): Semigroup[T] = ti.semigroup +\end{lstlisting} +The case class \lstinline!Monoid! combines a previous \lstinline!Semigroup! +instance with the new method \lstinline!empty!. + +Another possibility of implementing typeclass inheritance is to use +object-oriented inheritance of classes or traits. This approach is +often used when defining typeclasses as traits with methods: +\begin{lstlisting} +trait Semigroup[T] { def combine: (T, T) => T } +trait Monoid[T] extends Semigroup[T] { def empty: T } // `def combine` is inherited from `Semigroup`. +\end{lstlisting} +Creating an instance of \lstinline!Monoid! for a type \lstinline!T! +no longer requires having an instance of \lstinline!Semigroup! for +\lstinline!T!: + +\begin{wrapfigure}{l}{0.43\columnwidth}% +\vspace{-0.85\baselineskip} +\begin{lstlisting} +implicit val c: Monoid[Int] = new Monoid[Int] { + def empty: Int = 0 + def combine: (Int, Int) => Int = _ + _ +} +\end{lstlisting} + +\vspace{-1\baselineskip} +\end{wrapfigure}% + +\noindent With this approach, we cannot avoid repeating the code for +\lstinline!def combine...! even if we \emph{do} already have a \lstinline!Semigroup! +instance. However, conversion from \lstinline!Monoid[T]! to \lstinline!Semigroup[T]! +is now automatic due to \textbf{object-oriented inheritance}\index{object-oriented inheritance}: +\lstinline!Monoid[T]! is a subtype of \lstinline!Semigroup[T]! because +the \lstinline!Monoid! class is declared as \lstinline!extends Semigroup!. + +One problem with the object-oriented inheritance is that automatic +conversions to parent typeclasses cannot be disabled. When several +typeclasses inherit from the same parent typeclass, duplicate implicit +instances of the parent typeclass may be present. This will give a +compile-time error: +\begin{lstlisting} +trait TC[A] // Parent typeclass. +trait TC1[A] extends TC[A] // Two different typeclasses TC1 and TC2 inherit from TC. +trait TC2[A] extends TC[A] +// The function f requires A to have both TC1 and TC2 instances and then wants to access TC instance. +def f[A: TC1 : TC2]() = { + implicitly[TC[A]] // Compilation fails because two implicits of type TC[A] are found. +} +\end{lstlisting} + +When typeclass inheritance is implemented by combining instances, +conversions to parent typeclasses are not automatic \textendash{} +they are implemented by implicit functions that need to be explicitly +imported into the current scope. The programmer's code can avoid producing +duplicate instances by choosing which implicit conversions to use: +\begin{lstlisting} +final case class TC[A]() +final case class TC1[A]()(implicit val tc0: TC[A]) +object TC1 { implicit def toTC[A](implicit x: TC1[A]): TC[A] = x.tc0 } +final case class TC2[A]()(implicit val tc0: TC[A]) +object TC2 { implicit def toTC[A](implicit x: TC2[A]): TC[A] = x.tc0 } +// The function f requires A to have both TC1 and TC2 instances and then wants to access TC instance. +def f[A: TC1 : TC2]() = { + import TC1._ // Can import TC1._ or TC2._ but not both. If the next line is uncommented, +// import TC2._ // compilation will fail because two implicits of type TC[A] will be found! + implicitly[TC[A]] // This compiles successfully. One implicit instance of TC[A] can be found. +} +\end{lstlisting} + +The problem of duplicate inherited instances can be solved with less +work for the programmer at the cost of a more complicated encoding +of typeclasses.\footnote{See slides 5\textendash 14 of this talk by \index{John de Goes}John de Goes: \texttt{\href{https://www.slideshare.net/jdegoes/scalaz-8-a-whole-new-game}{https://www.slideshare.net/jdegoes/scalaz-8-a-whole-new-game}}} \begin{comment} diff --git a/sofp-src/sofp.pdf b/sofp-src/sofp.pdf index 677e30c2f..87205fabd 100644 Binary files a/sofp-src/sofp.pdf and b/sofp-src/sofp.pdf differ