Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inferring mutually-dependent default method implementations in Scala

Tags:

scala

traits

I'd like to define a trait with some properties which have a well defined relationship - for example's sake, let's say that a * b = c. The idea is that implementations of this trait can provide two of these and have an accessor for the the third property derived automatically.

(This is the same feature as Haskell's type classes, if I remember those correctly, where if you defined < from Ord, >= would be implemented as ! . < - though you could define any subset of functions so long as the remainder could be inferred.) (I don't remember Haskell's type classes correctly.)

The naïve approach actually works fairly well:

trait Foo {
  // a * b = c
  def a: Double = c / b
  def b: Double = c / a
  def c: Double = a * b
}

class FooOne(override val a: Double, override val b: Double) extends Foo
class FooTwo(override val a: Double, override val c: Double) extends Foo

Here, implementations FooOne and FooTwo are complete implementations of Foo, and behave as expected. So far, so good; this approach does allow classes to define two of the properties and get the third one "for free".

However, things start to look less rosy if one defines a third class:

class FooFail(override val a: Double) extends Foo

This compiles just fine - however, it will cause a stack overflow if its b or c methods are ever evaluated.


So the naïve approach gives the inference aspect of Haskell's type class approach, but we don't have compile-time safety. What I'd like is for the compiler to complain if less than two of the methods are defined by implementing classes. Clearly the current syntax isn't sufficient here; we need the methods to be considered abstract, albeit with a default implementation which can be used if and only if the dependent methods are non-abstract.

Does Scala expose appropriate semantics to define this? (I don't have a problem if there's a somewhat roundabout way of defining it, similar to union types, since I'm not aware of any first-class support for this in the language).

If not, I'll make do with the naïve approach and just define and test my classes carefully. But I really think this is something that the type system should be able to catch (after all - it's not Ruby. :)).

like image 238
Andrzej Doyle Avatar asked Sep 08 '11 09:09

Andrzej Doyle


3 Answers

Use implicits:

object test {
  case class A(v : Double)
  case class B(v : Double)
  case class C(v : Double)

  implicit def a(implicit b : B, c : C) = A(c.v / b.v)
  implicit def b(implicit a : A, c : C) = B(c.v / a.v)
  implicit def c(implicit a : A, b : B) = C(a.v * b.v)

  def foo(implicit a : A, b : B, c : C) = 
    a + ", " + b + ", " + c

  // Remove either of these and it won't compile
  implicit val aa = A(3)
  implicit val bb = B(4)

  def main(args : Array[String]) {
    println(foo)
  }
}
like image 69
Jesper Nordenberg Avatar answered Nov 02 '22 18:11

Jesper Nordenberg


How do you get safety in Haskell? I'm not very familiar with the language, but I can do

data Func = Func (Int -> Int) 
instance Eq Func
instance Ord Func

compared = Func (\i ->  i-1) < Func (\i -> i+1)

and get a stack overflow when evaluating compared.

I can imagine a workaround in scala but a rather feeble one. First, leave Foo fully abstract

trait Foo { def a: Double; def b: Double; def c: Double }

Then create mixin traits for each method definition combination allowed

trait FooWithAB extends Foo {def c : Double = a * b}
trait FooWithAC extends Foo {def b : Double = c / a}
trait FooWithBC extends Foo {def a : Double = c / b}

FooOne and FooTwo will have to mix in one of the trait. In FooFail, nothing prevents you to mix in two of them, and still fail, but you have been somewhat warned.

Is is possible to go one step further and forbid mixing two of them, with a kind of phantom type

trait Foo {
  type t;
  def a: Double; def b: Double; def c: Double
}
trait FooWithAB extends Foo {type t = FooWithAB; def c : Double = a * b}
trait FooWithAC extends Foo {type t = FooWithAC; def b : Double = c / a}
trait FooWithBC extends Foo {type t = FooWithBC; def c : Double = c / b}

This prevent mixing two of the FooWithXX, you cannot define

class FooFail(val a: Double) extends FooWithAC with FooWithAB
like image 33
Didier Dupont Avatar answered Nov 02 '22 16:11

Didier Dupont


A solution slightly weaker than what you may want is the following:

trait Foo {
  def a : Double
  def b : Double
  def c : Double
}

// Anything with two out of three can be promoted to Foo status.
implicit def ab2Foo(ab : { def a : Double; def b : Double }) =
  new Foo { val a = ab.a; val b = ab.b; def c = ab.a * ab.b }
implicit def bc2Foo(bc : { def b : Double; def c : Double }) =
  new Foo { val a = bc.c / bc.b; val b = bc.b; def c = bc.c }
implicit def ac2Foo(ac : { def a : Double; def c : Double }) =
  new Foo { val a = ac.a; val b = ac.c / ac.a; def c = ac.c }

Here, any class with two of the three a, b, c methods can be viewed and used as a Foo. For example:

case class AB(a : Double, b : Double)
AB(5.0, 7.1).c // prints 35.5

But if you try for instance:

case class ABC(a : Double, b : Double, c : Double)
val x : Foo = ABC(1.0, 2.0, 3.0)

...you get an "ambiguous implicit" error.

The main problem w.r.t your original statement is that the classes don't properly inherit from Foo, which may be a problem if you wanted to reuse other methods. You can work around it though by having another trait FooImpl that contains these other methods, and restrict the implicit conversions to subtypes of FooImpl.

like image 20
Philippe Avatar answered Nov 02 '22 17:11

Philippe