Kombajn zbożowy
Kombajn zbożowy

Reputation: 10703

Implement trait methods using subclasses' types

I want a trait Foo to provide transform method that would apply a function to it. Also, I want to force implementing classes to have an increment method that would somehow transform the object as well. Naive solution:

trait Foo {
  def transform(fun: Foo => Foo): Foo = fun(this)
  def increment(n: Int): Foo
}

case class A(a: Int) extends Foo {
  // expecting available: transform(fun: A => A): A
  // to be implemented: increment(n: Int): A
  ...
}

Above won't work... Inherited transform still expects Foo => Foo, not A => A and increment still wants to return Foo, not A.

One more attempt:

trait Foo {
  def transform[C <: Foo](fun: C => C): C = fun(this.asInstanceOf[C])
  def increment[C <: Foo](n: Int): C
}

case class A(a: Int) extends Foo {
  def increment(n: Int) = A(a + n)
}

A will not compile - it will still complain about the signature.

Taking out the increment function, transform works. However asInstanceOf looks a bit unsafe. Also, I need to explicitly provide type parameter to transform:

val a = A(1)
a.transform[A](x => x.copy(x.a + 1)) // returns A(2)

I wonder if there's a smart way to have it done.

Upvotes: 3

Views: 210

Answers (1)

Ethan
Ethan

Reputation: 841

The most direct way of getting what you want is to move the type parameter up to the trait declaration. That gives trait Foo[C]{...}. However, using copy in your transform still won't work, since the Foo trait doesn't know anything about anything that extends it. You can give it a bit more information with self typing:

trait Foo[C] {
  this: C =>
    def transform(fun: C => C): C = fun(this)
    def increment(n: Int): C
}

case class A(a: Int) extends Foo[A] {
  def increment(n: Int) = A(a + n)
}

The A extends Foo[A] is a little awkward to use here, but it works, since now when you extend Foo, it provides that type information back to the trait. That's still a little bit awkward though. It turns out there's a technique called type classes that we can use here to potentially improve things. First, you set up your trait. In a type class, there is exactly one implementation of the trait per type, so each method should also take in the instance that you want to operate on:

trait Foo[C] {
  def transform(c: C)(f: C => C): C
  def increment(c: C, inc: Int): C
}

Next, in the companion object you set up instances of the typeclass for the types you care about:

case class A(a: Int)

object Foo {
  implicit val ATransform = new Foo[A] {
    def transform (base: A)(f: A => A) = f(base)
    def increment(base: A, inc: Int) = A(base.a+inc)
  }

  //Convenience function for finding the instance for a type.
  //With this, Foo[A] is equivalent to implicitly[Foo[A]]
  def apply[C](implicit foo: Foo[C]) = foo
}

Now we can use the type class as follows:

val b = A(3)
Foo[A].transform(b)(x=>x.copy(a=x.a+1)) //A(4)
Foo[A].increment(b,5) //A(8)

Upvotes: 5

Related Questions