scalagenericstraitsscala-3f-bounded-polymorphism

How to restrict generic trait in subtype


I have a trait Mutable[T] that describes objects that can be mutated to T using a Mutation object:

trait Mutable[T] {
  def mutate(mutation: Mutation): T
}

class Mutation {
  def perform[T <: Mutable[T]](mutable: T): T = mutable.mutate(this)
}

I also have two traits describing animals in general, as well as specifically mammals.

I would like to require that an Animal can mutate into another Animal, but a Mammal can only mutate into another Mammal. However, the following does not compile:

trait Animal extends Mutable[Animal]
trait Mammal extends Animal, Mutable[Mammal]

case class Fish() extends Animal {
  override def mutate(mutation: Mutation): Animal = Fish()
}

// error: class Monkey cannot be instantiated since it has conflicting base types Mutable[Animal] and Mutable[Mammal]
case class Monkey() extends Mammal {
  override def mutate(mutation: Mutation): Mammal = Human()
}

// error: class Human cannot be instantiated since it has conflicting base types Mutable[Animal] and Mutable[Mammal]
case class Human() extends Mammal {
  override def mutate(mutation: Mutation): Mammal = Monkey()
}

I would like to use these types as follows:

val mutation = new Mutation()

val fish: Animal = Fish()
val fish2: Animal = mutation.perform(fish)

val monkey: Mammal = Monkey()
val monkey2: Mammal = mutation.perform(monkey)

Solution

  • Don't you want to make Mutable covariant?

    trait Mutable[+T] {
      def mutate(mutation: Mutation): T
    }
    

    In such case your code seems to compile in Scala 3

    https://scastie.scala-lang.org/qVMDsu7HRLiBFlSchGxWEA


    When you were loosening the restriction on Mutation.mutate to [T <: Mutable[? <: T]] Mutable[? <: T] is actually defining covariance at a call site

    In Scala3, if generic type argument(s) is mapped to dependent type, how are covariant & contravariant modifiers mapped?


    Also you can try to make T a type member rather than type parameter. In such case the existential type is just Mutable while a specific type is Mutable { type T = ... } (aka Mutable.Aux[...])

    trait Mutable:
      type T
      def mutate(mutation: Mutation): T
    
    object Mutable:
      type Aux[_T] = Mutable { type T = _T }
    
    class Mutation:
      def perform[M <: Mutable](mutable: Mutable): mutable.T = mutable.mutate(this)
    
    trait Animal extends Mutable:
      type T <: Animal
    
    trait Mammal extends Animal:
      type T <: Mammal
    
    case class Fish() extends Animal:
      type T = Animal
      override def mutate(mutation: Mutation): Animal = Fish()
    
    case class Monkey() extends Mammal:
      type T = Mammal
      override def mutate(mutation: Mutation): Mammal = Human()
    
    case class Human() extends Mammal:
      type T = Mammal
      override def mutate(mutation: Mutation): Mammal = Monkey()
    
    val mutation = new Mutation()
    
    val monkey: Mammal = Monkey()
    val monkey2: Mammal = mutation.perform(monkey)
    val monkey3: Mammal = mutation.perform[Mammal](monkey)
    
    val fish: Animal = Fish()
    val fish2: Animal = mutation.perform(fish)
    val fish3: Animal = mutation.perform[Animal](fish)
    

    Bind wildcard type argument in Scala (answer)


    Also you can try a type class

    // type class
    trait Mutable[T]:
      type Out
      def mutate(t: T, mutation: Mutation): Out
    
    class Mutation:
      def perform[T](t: T)(using mutable: Mutable[T]): mutable.Out = mutable.mutate(t, this)
    
    trait Animal
    trait Mammal extends Animal
    
    case class Fish() extends Animal
    
    object Fish:
      given Mutable[Fish] with
        type Out = Fish
        def mutate(t: Fish, mutation: Mutation): Out = Fish()
    
    case class Monkey() extends Mammal
    
    object Monkey:
      given Mutable[Monkey] with
        type Out = Human
        def mutate(t: Monkey, mutation: Mutation): Out = Human()
    
    case class Human() extends Mammal
    
    object Human:
      given Mutable[Human] with
        type Out = Monkey
        def mutate(t: Human, mutation: Mutation): Out = Monkey()
    
    val mutation = new Mutation()
    
    val monkey: Monkey = Monkey()
    val monkey2: Human = mutation.perform(monkey)
    val monkey3: Human = mutation.perform[Monkey](monkey)
    
    val fish: Fish = Fish()
    val fish2: Fish = mutation.perform(fish)
    val fish3: Fish = mutation.perform[Fish](fish)
    

    Although type class instances have to be resolved statically while you seem to prefer resolving values dynamically (val fish: Animal = Fish, val monkey: Mammal = Monkey).

    https://docs.scala-lang.org/tutorials/FAQ/index.html#how-can-a-method-in-a-superclass-return-a-value-of-the-current-type

    http://tpolecat.github.io/2015/04/29/f-bounds.html

    Advantages of F-bounded polymorphism over typeclass for return-current-type problem