Unifying inner classes across different parent instances

67 views Asked by At

EDIT: Added in information about F[_]

Here's the setup. We have a parent class which contains a type, and a method which processes a type projection of this, across all parent instances:

class Parent[F[_]] {

  // These depend on information from Parent (ie F) so
  // cannot be moved outside
  trait Inner { def execute[A]: F[A] }
  case class Foo(i: Int) extends Inner { ... }
  case class Bar(s: String) extends Inner { ... }

  def process(value: Parent#Inner): Unit = value match {
    case Foo(_) => println("integer")
    case Bar(_) => println("string")
  }

}

The problem is that inside the case match, references to Foo and Bar are this.Inner, rather than Parent#Inner. So the following fails:

val foo = (new Parent[IO]).Foo(5)
val processer = new Parent[IO]
processer.process(foo)  // match error

One way around this is to change def process to:

  def process(value: Parent#Inner): Unit = value.asInstanceOf[this.Inner] match {
    case Foo(_) => println("integer")
    case Bar(_) => println("string")
  }

(note the new .asInstanceOf).

However, this is unsatisfying.

Beyond extracting def process out to a third-party class somewhere, is there a nicer way to achieve our desired behaviour?

EDIT:

The classes unfortunately need to be defined inside Parent due to the dependence on the F[_]. We could in theory move them outside as the initial answer suggests but this would introduce too much work and variety elsewhere since we would need to parameterise each Inner subclass by an F[_]

EDIT 2:

One potential solution is to reformulate process like this:

def process(value: Parent#Inner): Unit = value match {
  case _: Parent[F]#Foo => println("integer")
  case _: Parent[F]#Bar => println("string")
}

But this means we cannot use Foos unapply method. The following is invalid:

case Parent[F]#Foo(_) => println("integer")

In the case where Foo is instead for example Foo[A, B, C](a: A, b: B, c: C) this would mean the match statement becomes:

case _: Parent[F]#Foo[A, B, C] @unchecked => ...

which introduces a lot more complexity and potential for failure into the pattern match.

2

There are 2 answers

1
Alexey Romanov On

You can use type projections in the pattern match:

def process(value: Parent[F]#Inner): Unit = value match {
  case _: Parent[F]#Foo => println("integer")
  case _: Parent[F]#Bar => println("string")
}

Another approach which does let you use unapply:

// Start writing your ScalaFiddle code here
class Parent[F[_]] { self =>

  // These depend on information from Parent (ie F) so
  // cannot be moved outside
  trait Inner { def parent = self }
  case class Foo(i: Int) extends Inner
  case class Bar(s: String) extends Inner

  def process(value: Parent[F]#Inner): Unit = {
    val parent = value.parent
    value match {
      case parent.Foo(_) => println("integer")
      case parent.Bar(_) => println("string")
    }
  }

}

val foo = (new Parent[List]).Foo(5)
val processer = new Parent[List]
processer.process(foo) // integer

You can also use value.parent.Foo(_) as a pattern if you make Inner#parent a val.

3
Dmytro Mitin On

You should write either with path-dependent types

class Parent[F[_]] {
  trait Inner {
    def execute[A]: F[A]
  }

  case class Foo(i: Int) extends Inner {
    override def execute[A]: F[A] = ???
  }

  case class Bar(s: String) extends Inner {
    override def execute[A]: F[A] = ???
  }

  def process(value: Inner): Unit = value match {
    case Foo(_) => println("integer")
    case Bar(_) => println("string")
  }
}

val processer = new Parent[IO]
val foo: processer.Inner = processer.Foo(5)
processer.process(foo)

or with type projections

class Parent[F[_]] {
  trait Inner {
    def execute[A]: F[A]
  }

  case class Foo(i: Int) extends Inner {
    override def execute[A]: F[A] = ???
  }

  case class Bar(s: String) extends Inner {
    override def execute[A]: F[A] = ???
  }

  def process(value: Parent[F]#Inner): Unit = value match {
    case _: Parent[F]#Foo => println("integer")
    case _: Parent[F]#Bar => println("string")
  }
}

val foo: Parent[IO]#Inner = new Parent[IO].Foo(5)
val processer = new Parent[IO]
processer.process(foo) 

Example with parametrized Foo without unchecked type matching:

class Parent[F[_]] {
  trait Inner {
    def execute[A]: F[A]
  }

  case class Foo[B](i: Int) extends Inner {
    override def execute[A]: F[A] = ???
  }

  case class Bar(s: String) extends Inner {
    override def execute[A]: F[A] = ???
  }

  def process(value: Parent[F]#Inner): Unit = value match {
    case _: Parent[F]#Foo[_] => println("integer")
    case _: Parent[F]#Bar => println("string")
  }
}