Reputation: 10703
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
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