4

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] {}
Ava
  • 818
  • 10
  • 18
  • 1
    That won't work, your `foo` method has to pick one (and only one) `T` to work with. You could use typeclasses instead of inheritance. That way you can supply three different versions: `IngredientFoo[Pizza, Cheese]`, `IngredientFoo[Pizza, Pepperoni]` and `IngredientFoo[Pizza, Oregano]` and call a `def foo[X,T](food: X, ingredient:T)(implicit handler: IngredientFoo[X,T]) = handler.foo(food, ingredient)` – Thilo Jan 17 '21 at 13:40
  • 1
    You'd only do that if you want to keep both the food and the ingredients independently extensible. If not, just code directly to the known types you have: `def fooCheese` etc – Thilo Jan 17 '21 at 13:41
  • 1
    You probably need to give some more context on what you are trying to do here. – Thilo Jan 17 '21 at 13:44
  • 1
    It would be better to explain the meta-problem you are trying to model, so we can propose alternatives. – Luis Miguel Mejía Suárez Jan 17 '21 at 14:54

4 Answers4

3

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

Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
  • I don't know... You can now also call `pizza.foo(123)`. The parameter is no longer bound to be an `Ingredient`. And `pizza.foo(pizza)` prints `Oregano`. That does not seem very useful. – Thilo Jan 17 '21 at 13:50
  • Now you can call `pizza.foo(new Cucumbers {})` (another Ingredient totally unrelated to pizza's own) – Thilo Jan 17 '21 at 14:12
  • @Thilo, to resolve that you ca define an intermediate trait. The sane I showed in my seconds option. – Tomer Shetah Jan 17 '21 at 15:37
  • That's a nice solution! I didn't have an option to modify Ingredient trait in my case. I didn't specify that in my question but it's maybe better that we have multiple solutions here. I've already posted mine in an answer when posting the question because I didn't find a similar problem when searching for it, maybe it will help someone. – Ava Jan 18 '21 at 11:22
  • One additional thing I noticed in solution 1: here `trait Pizza extends Ingredient` can be used instead of `trait Pizza extends Pepperoni with Cheese with Oregano`. – Ava Jan 18 '21 at 11:34
2

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.

Ava
  • 818
  • 10
  • 18
1

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
   }

 }
Thilo
  • 257,207
  • 101
  • 511
  • 656
0

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)]
Helix112
  • 305
  • 3
  • 12