How to inherit generic trait multiple times in Scala?

Viewed 276

I have a trait that looks like this:

trait Ingredient[T] {
  def foo(t: T): Unit = {
    // Some complex logic
  }
}

And types for which I want to have methods:

class Cheese
class Pepperoni
class Oregano

How can I make another trait that has methods:

def foo(t: Cheese)
def foo(t: Pepperoni)
def foo(t: Oregano)

without duplicating the code? The following will not work as it has illegal inheritance from the same trait multiple times:

trait Pizza extends Ingredient[Cheese] with Ingredient[Pepperoni] with Ingredient[Oregano] {}
4 Answers

I'll offer 2 solutions:

  1. If you are fine with defining Cheese, Pepperoni, and Oregano as traits, you can do:

    trait Ingredient {
       def foo[T <: Ingredient](t: T): Unit = {
         println(t)
       }
     }
    

    Then extending it:

    trait Cheese extends Ingredient {
      override def toString: String = "Cheese"
    }
    trait Pepperoni extends Ingredient {
      override def toString: String = "Pepperoni"
    }
    trait Oregano extends Ingredient {
      override def toString: String = "Oregano"
    }
    

    And the usage is:

    trait Pizza extends Ingredient
    
    val pizza = new Pizza { }
    
    pizza.foo(new Cheese { })
    pizza.foo(new Pepperoni { })
    pizza.foo(new Oregano { })
    

    Code run at Scastie.

  2. Using sealed traits. This approach separates the ingredients from the final product which is binded in the question:

    sealed trait Ingredient
    sealed trait PizzaIngredient extends Ingredient
    case object Cheese extends PizzaIngredient
    case object Pepperoni extends PizzaIngredient
    case object Oregano extends PizzaIngredient
    case object Cucumber extends Ingredient
    

    Then define the Pizza trait:

    trait Pizza {
        def foo[T <: PizzaIngredient](t: T): Unit = {
          println(t)
        }
    }
    

    And the usage is:

    val pizza = new Pizza { }
    pizza.foo(Cheese)
    pizza.foo(Pepperoni)
    pizza.foo(Oregano)
    

    Code run at Scastie

The solution is to create objects that extend the Ingredient trait in Pizza trait. This way we have:

trait Pizza {
  object CheeseIngredient extends Ingredient[Cheese]
  object PepperoniIngredient extends Ingredient[Pepperoni]
  object OreganoIngredient extends Ingredient[Oregano]
}

Then we can call our methods on inherited objects:

object LargePizza extends Pizza {
  def bar(cheese: Cheese, pepperoni: Pepperoni, oregano: Oregano): Unit = {
    CheeseIngredient.foo(cheese)
    PepperoniIngredient.foo(pepperoni)
    OreganoIngredient.foo(oregano)
  }
}

Alternatively if we have only a few methods in Ingredient trait we can create overloads and encapsulate our supporting objects:

trait Pizza {
  private object CheeseIngredient extends Ingredient[Cheese]
  private object PepperoniIngredient extends Ingredient[Pepperoni]
  private object OreganoIngredient extends Ingredient[Oregano]

  def foo(cheese: Cheese): Unit = CheeseIngredient.foo(cheese)
  def foo(pepperoni: Pepperoni): Unit = PepperoniIngredient.foo(pepperoni)
  def foo(oregano: Oregano): Unit = OreganoIngredient.foo(oregano)
}

object LargePizza extends Pizza {
  def bar(cheese: Cheese, pepperoni: Pepperoni, oregano: Oregano): Unit = {
    foo(cheese)
    foo(pepperoni)
    foo(oregano)
  }
}

The same could be achieved by just using imports in LargePizza. Adding overloads is better though as the client does not need to write additional imports and does not have the supporting objects in scope. The other benefit of using overloads is that the methods can be overridden in child classes.

If the intention is that Pizza needs all three of these ingredients, how about

class Pizza extends Ingredient[(Cheese, Pepperoni, Oregano)] {

   def foo(ingredients: (Cheese, Pepperoni, Oregano)) = {
      case (cheese, pepperoni, oregano) =>
          // do something with them
   }

 }

Maybe you can do the following , you'll have to change the foo method :

    trait Ingredient[T] {

   def foo(ingredients: T*): Unit = {
     for (i <- ingredients) {
        // some complex logic
      }
   }
}
    class Cheese 
    class Pepperoni 
    class Oregano 

 
trait Pizza  extends Ingredient[(Cheese,Pepperoni,Oregano)]
Related