Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning the same type from abstract trait method

Lets say we have a trait, that has some values and some operations on them.

trait Foo {
  type Self <: Foo
  val x: Int
  def withX(x: Int): Self
}

This is implemented using abstract types. We have a type bound on Self and can implement it like this:

case class Foo1(x: Int) extends Foo {
  type Self = Foo1
  def withX(x: Int) = copy(x = x)
}

That is fine. We can use the method and we see that the type is statically kept.

scala> Foo1(10).withX(5)
res0: Foo1 = Foo1(5)

Problems start when we want to have an operation with trait type, not the concrete type:

object Foo {
//Error:(13, 43) type mismatch;
//found   : f.Self
//required: A
//  def setFive[A <: Foo](f: A): A = f.withX(5)
}

Well, we can't quite do it, because the compiler does not know what type Foo#Self will be assigned to. But we know it is the same type.

Of course using an ugly approach works fine:

object Foo {
  // Ugly type signature
  def setFiveValid[A <: Foo](f: A): A#Self = f.withX(5)

  // Another ugly type signature
  def setFiveValid2[A <: Foo](f: A): f.Self = f.withX(5)
}

Neither of them express intent very clearly.

We can work around it using typeclasses though.

case class Foo2(x: Int)

trait FooOps[A] extends Any {
  def a: A
  def withX(x: Int): A
}

object Foo2 {
  implicit class Foo2Ops(val a: Foo2) extends AnyVal with FooOps[Foo2] {
    def withX(x: Int) = a.copy(x = x)
  }
}

object Foo {
  // View bounds approach.
  def setFiveValid3[A <% FooOps[A]](f: A): A = f.withX(5)
}

However this is still very noisy.

Is there a better way to implement setFive?

Edit 1

The main issue with self types are things like this:

Error:(24, 11) type mismatch;
 found   : app.models.world.WObject.WorldObjUpdate[self.Self] => app.models.world.WObject.WorldObjUpdate[self.Self]
    (which expands to)  app.models.game.events.Evented[(app.models.world.World, self.Self)] => app.models.game.events.Evented[(app.models.world.World, self.Self)]
 required: app.models.world.WObject.WorldObjUpdate[self.Self] => app.models.game.events.Evented[(app.models.world.World, Self)]
    (which expands to)  app.models.game.events.Evented[(app.models.world.World, self.Self)] => app.models.game.events.Evented[(app.models.world.World, Self)]
          identity
          ^

Which then resort to weird looking signatures and boilerplate again:

  def attackReachable(
    data: WObject.WorldObjUpdate[Self]
  ): WObject.WorldObjUpdate[data.value._2.Self]
like image 585
arturaz Avatar asked Sep 29 '14 17:09

arturaz


2 Answers

You could go down the "F-bounded quantification" road:

trait Foo[F <: Foo[F]] {
  def withX(x: Int): F
}

object Foo {
  def setFive[F <: Foo[F]](f: F): F = f.withX(5)
}

I used this a lot with success, but it comes at the price of having to write F <: Foo[F]] everwhere.

like image 125
0__ Avatar answered Nov 15 '22 06:11

0__


The best signature is the path-dependent type that you suggested:

// Another ugly type signature
def setFiveValid2[A <: Foo](f: A): f.Self = f.withX(5)

You don't even need the type parameter. Typing f as Foo will do (unless you need A for something else in your real context):

def setFiveValid3(f: Foo): f.Self = f.withX(5)

This is not ugly. It's one of the perfect uses for path-dependent types, on the contrary. I also disagree when you say that it doesn't express the intent clearly: you're clearly stating that the result will have the type Self of the argument that you're giving.

like image 20
sjrd Avatar answered Nov 15 '22 08:11

sjrd