Generic function where the return type depends on the input type in Scala?
Asked Answered
S

2

3

I'm trying make this code to compile:

import cats.effect.IO

sealed trait Shape {
  val x: Int
}

case class Square(x: Int, y: Int) extends Shape
case class Cube(x: Int, y: Int, z: Int) extends Shape

def modifyShape[S <: Shape](shape: S): IO[S] = shape match {
  case s: Square => IO(s.copy(y = 5))
  case c: Cube => IO(c.copy(z = 5))
}

When I'm trying to compile this code I'm getting error:

type mismatch;
found : Square
required: S
case s: Square => IO(s.copy(y = 5))

How to make this code work?

Update:
After reading comments and articles I tried to use F-bound like this:

sealed trait Shape[A <: Shape[A]] { this: A =>
  val x: Int
}

case class Square(x: Int, y: Int) extends Shape[Square]
case class Cube(x: Int, y: Int, z: Int) extends Shape[Cube]

def modifyShape[S <: Shape[S]](shape: S): IO[S] = shape match {
  case s: Square => IO(s.copy(y = 5))
  case c: Cube => IO(c.copy(z = 5))
}

But seems I missed something. This still doesn't work.

Samhita answered 12/10, 2020 at 12:44 Comment(4)
Why is the monomorphic signature def modifyShape(shape: Shape): IO[Shape] not enough?Futrell
tpolecat.github.io/2015/04/29/f-bounds.htmlMaggot
@DmytroMitin Because I need real subtype to use it in other functionSamhita
Ok. Thanks you. Would you like to add an answer?Samhita
F
3

Now modifyShape's body

shape match {
  case s: Square => IO(s.copy(y = 5))
  case c: Cube => IO(c.copy(z = 5))
}

just doesn't satisfy its signature

def modifyShape[S <: Shape](shape: S): IO[S] 

See details here:

Why can't I return a concrete subtype of A if a generic subtype of A is declared as return parameter?

Type mismatch on abstract type used in pattern matching

foo[S <: Shape] means that foo has to work for any S that is a subtype of Shape. Suppose I take S := Shape with SomeTrait, you don't return IO[Shape with SomeTrait].

Try GADT with F-bounded type parameter

sealed trait Shape[S <: Shape[S]] { this: S =>
  val x: Int
  def modifyShape: IO[S]
}

case class Square(x: Int, y: Int) extends Shape[Square] {
  override def modifyShape: IO[Square] = IO(this.copy(y = 5))
}
case class Cube(x: Int, y: Int, z: Int) extends Shape[Cube] {
  override def modifyShape: IO[Cube] = IO(this.copy(z = 5))
}

def modifyShape[S <: Shape[S]](shape: S): IO[S] = shape.modifyShape

https://tpolecat.github.io/2015/04/29/f-bounds.html (@LuisMiguelMejíaSuárez reminded the link)

or GADT with F-bounded type member

sealed trait Shape { self =>
  val x: Int
  type S >: self.type <: Shape { type S = self.S }
  def modifyShape: IO[S]
}

case class Square(x: Int, y: Int) extends Shape {
  override type S = Square
  override def modifyShape: IO[Square] = IO(this.copy(y = 5))
}
case class Cube(x: Int, y: Int, z: Int) extends Shape {
  override type S = Cube
  override def modifyShape: IO[Cube] = IO(this.copy(z = 5))
}

def modifyShape[_S <: Shape { type S = _S}](shape: _S): IO[_S] = shape.modifyShape
// or  
// def modifyShape(shape: Shape): IO[shape.S] = shape.modifyShape

or GADT (without F-bound)

(see details in @MatthiasBerndt's answer and my comments to it, this code portion is from his answer)

sealed trait Shape[A] {
  val x: Int
}

case class Square(x: Int, y: Int) extends Shape[Square]
case class Cube(x: Int, y: Int, z: Int) extends Shape[Cube]

def modifyShape[S](shape: Shape[S]): IO[S] = shape match {
  case s: Square => IO(s.copy(y = 5))
  case c: Cube   => IO(c.copy(z = 5))
}

or ADT + reflection

sealed trait Shape {
  val x: Int
}

case class Square(x: Int, y: Int) extends Shape
case class Cube(x: Int, y: Int, z: Int) extends Shape

import scala.reflect.runtime.universe._

def modifyShape[S <: Shape : TypeTag](shape: S): IO[S] = (shape match {
  case s: Square if typeOf[S] <:< typeOf[Square] => IO(s.copy(y = 5))
  case c: Cube   if typeOf[S] <:< typeOf[Cube]   => IO(c.copy(z = 5))
}).asInstanceOf[IO[S]]

or ADT + type class

sealed trait Shape {
  val x: Int
}

case class Square(x: Int, y: Int) extends Shape
case class Cube(x: Int, y: Int, z: Int) extends Shape

trait ModifyShape[S <: Shape] {
  def modifyShape(s: S): IO[S]
}
object ModifyShape {
  implicit val squareModifyShape: ModifyShape[Square] = s => IO(s.copy(y = 5))
  implicit val cubeModifyShape:   ModifyShape[Cube]   = c => IO(c.copy(z = 5))
}

def modifyShape[S <: Shape](shape: S)(implicit ms: ModifyShape[S]): IO[S] =
  ms.modifyShape(shape)

or ADT + magnet

sealed trait Shape {
  val x: Int
}

case class Square(x: Int, y: Int) extends Shape
case class Cube(x: Int, y: Int, z: Int) extends Shape

import scala.language.implicitConversions

trait ModifyShape {
  type Out
  def modifyShape(): Out
}
object ModifyShape {
  implicit def fromSquare(s: Square): ModifyShape { type Out = IO[Square] } = new ModifyShape {
    override type Out = IO[Square]
    override def modifyShape(): IO[Square] = IO(s.copy(y = 5))
  }
  implicit def fromCube(c: Cube): ModifyShape { type Out = IO[Cube] } = new ModifyShape {
    override type Out = IO[Cube]
    override def modifyShape(): IO[Cube] = IO(c.copy(z = 5))
  }
}

def modifyShape(shape: ModifyShape): shape.Out = shape.modifyShape()
Futrell answered 12/10, 2020 at 18:0 Comment(0)
V
2

The solution here is to use a GADT, a generalized algebraic datatype.

In a normal (non-generalized) ADT, the case classes will take exactly the same type parameters as the sealed trait and pass them through unmodified like in this example:

sealed trait Either[A, B]
case class Left[A, B](a: A) extends Either[A, B]
case class Right[A, B](b: B) extends Either[A, B]
// both Left and Right take two type parameters, A and B,
// and simply pass them through to sealed trait Either. 

In a generalized ADT, there is no such restriction. Therefore, Square and Cube are allowed to take a different set of type parameters than Shape (in this case the empty set, meaning none at all), and they can fill in the type parameter of Shape with something other than their own type parameters. In this case, because they don't have any type parameters that they could pass through to Shape, they just pass their own type.

sealed trait Shape[A] {
  val x: Int
}

case class Square(x: Int, y: Int) extends Shape[Square]
case class Cube(x: Int, y: Int, z: Int) extends Shape[Cube]

With this declaration, the following definition will compile:

  def modifyShape[S](shape: Shape[S]): IO[S] = shape match {
    case s: Square => IO(s.copy(y = 5))
    case c: Cube => IO(c.copy(z = 5))
  }

When the Scala compiler sees that shape is in fact a Square, it is smart enough to figure out that S must be Square, because that is what the Square case class passed as a type parameter to Shape.

But it is by no means necessary that Square and Cube pass their own type to Shape as a type parameter. For instance, they could pass the other one like in this example:

  sealed trait Shape[A] {
    val x: Int
  }

  case class Square(x: Int, y: Int) extends Shape[Cube]
  case class Cube(x: Int, y: Int, z: Int) extends Shape[Square]

  def changeDimension[S](shape: Shape[S]): IO[S] = shape match {
    case s: Square => IO(Cube(s.x, s.y, 42))
    case c: Cube => IO(Square(c.x, c.y))
  }


  val x: IO[Square] = changeDimension(Cube(3, 6, 25))

Vadim answered 12/10, 2020 at 22:37 Comment(2)
Possibility of changeDimension can be considered as an advantage of GADT approach (over F-bound approach for example) if a user is interested in such kind of flexibility/modification or as a disadvantage if a user is interested in rigidity (returning "current" type, making changeDimension impossible).Futrell
By the way, if we add F-bound to the trait Shape and method modifyShape (in order to exclude situation with Square extends Shape[Cube] and changeDimension) then GADT code stops to compile.Futrell

© 2022 - 2024 — McMap. All rights reserved.