Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Accept any case class which extends a trait as argument in scala

Tags:

scala

I need a function to accept any case class which extends a trait. To give an example

Suppose there is a trait

trait Action {
    def execute(param: Parameters)
}

And following parameters can be passed depending on what type of action it is

trait Parameters
case class A(x: String, y: Double) extends Parameters
case class B(x: Int, y:String, z: Double) extends Parameters

User should be able to specify what it wants as parameters

class Action1 extends Action {
    override def execute(param: A)
}

class Action2 extends Action {
    override def execute(param: B)
}

and while calling

def run(t: Action) {
    t.execute(new A("a", 1.0))
}

How to achieve this? When I implement Action1, it says I will get error that we cannot override because trait says execute method accepts Parameters, but while overriding, we want a class which extends Parameters.

like image 367
chinmayv Avatar asked Mar 26 '16 09:03

chinmayv


2 Answers

One way is to parametrize Action.

trait Action[T <: Parameters] {
  def execute(param: T)
}

Then

class Action1 extends Action[A] {
  override def execute(param: A)
}

class Action2 extends Action[B] {
  override def execute(param: B)
}

def run[T <: Parameters](t: Action[T], param: T) {
  t.execute(param)
}

// You don't need new for a case class
run(new Action1, A("a", 1.0))

You have to modify run a little bit here because you can't get what you're asking for by subclassing alone. Argument types are contravariant, meaning that something that a function A => T is a superclass of a function Parameters => T. This is because while any argument of type A works for both A => T and Parameters => T, only some arguments of type Parameters will work for A => T while all of them will work for Parameters => T.

like image 78
badcook Avatar answered Sep 27 '22 00:09

badcook


The thing you need to think about is why do you need your Action to have the execute method to begin with. Probably, you have something like this elsewhere in your code:

def executeAll(actions: Seq[Action], params: Parameters) = 
    actions.foreach { _.execute(params) }

Note, that here your actions list can contain actions of different types. if some of them except the parameter of type A, and some need it to be of type B, it won't work, because your params parameter cannot be both.

For this reason, you cannot override a method to take a different parameter type in a subclass (actually, you can, but the type has to be a super-class of the original type, not a subclass. They say that functions are contravariant in their parameter types for this reas, but I digress).

If your Action1 and Action2 can actually deal with their parameters generically, through the trait interface without requiring them to be of a particular type, then the solution is simple: just change the declarations of your overriding methods to be override def execute(params: Parameters).

If this is not the case, and Action1 can only deal with parameters of type A, then (1) there is, probably (although, not necessarily) something wrong with your design, and (2) you have to go with parametrization solution, as the other answer suggests. But then you would have to change all the places, that deal with Action as well:

def executeAll[T <: Parameters](actions: Seq[Action[T]], params: T) = ...

This declaration constraints the type of the params argument to match what the actions in the list expect, so that Action1.execute will not be called with B by mistake.

Now, as you mentioned in the comment to the other answer, you can't mix actions of different type, because it does not make sense: they are essentially different classes. If you are not going to use the execute method, and just want to pass the list of different actions around some piece of code, you can use the wildcard syntax:

val actions: List[Action[_] = List(Action1(...), Action2(...))

Once again, this list can serve as a generic container for actions of any type, but you cannot use it to apply the execute method generically, because that would require the parameter to be of different types at the same time.

like image 25
Dima Avatar answered Sep 27 '22 00:09

Dima