Legat
Legat

Reputation: 1449

Scala overloading not choosing most specific method

Having Java classes:

class abstract Exp
class ExpAdd extends Exp
class ExpSub extends Exp

and Scala methods:

def interpret(e: ExpAdd) = interpret(e.left_arg) + interpret(e.right_arg)
def interpret(e: ExpSub) = interpret(e.left_arg) - interpret(e.right_arg)

def interpret(exp: Exp) = exp match {
  case e : ExpAdd = interpret(e)
  case e : ExpSub = interpret(e)
}

How to refactor the code to make adding new Exps easier? I thought interpret(Exp) wouldn't be used at all as two other are more specific and

If more than one member method is both accessible and applicable to a method invocation, it is necessary to choose one to provide the descriptor for the run-time method dispatch. The Java programming language uses the rule that the most specific method is chosen.

however that seems not to be the case in Scala. I also tried removing interpret(Exp) but compiler didn't like it. That's why I used case clause... is there any way to make overloading work as expected? or maybe some workaround?

Upvotes: 1

Views: 211

Answers (3)

lmm
lmm

Reputation: 17431

As @Alexy says, the method resolution is based on the compile-time or "static" type of the arguments. You could make the types of the operands part of the type of the expression, and then use implicits to recursively construct the interpreter for a given type:

trait Exp
case class ExpAdd[A <: Exp, B <: Exp](left_arg: A, right_arg: B) extends Exp
case class ExpSub[A <: Exp, B <: Exp](left_arg: A, right_arg: B) extends Exp
case class Literal(i: Int) extends Exp

trait ExpInterpreter[E <: Exp] {
  def interpret(e: E): Int
}
object ExpInterpreter {
  implicit object LiteralInterpreter extends Interpreter[Literal] {
    def interpret(l: Literal) = l.i
  }
  implicit def addInterpreter[A <: Exp, B <: Exp](
    implicit leftInterpreter: Interpreter[A], rightInterpreter: Interpreter[B]) =
    new Interpreter[ExpAdd[A, B]] {
      def interpret(e: ExpAdd[A, B]) = leftInterpreter.interpret(e.left_arg) +
        rightInterpreter.interpret(e.right_arg)
    }
  implicit def subInterpreter ...
}

Then when you add new Exp cases you just need to define a new implicit, and you can do that anywhere rather than having to add a case to a particular match expression. You could even have Exp and ExpInterpreter in a library and the cases and specific interpreters in a different jar.

As written this means the structure of an expression is part of its type (and you have to carry the type around until you interpret it - there's no way to interpret a generic Exp, only an E <: Exp: ExpInterpreter), but maybe it's possible to use a similar technique to the Free monad to overcome this?

Upvotes: 3

Alexey Romanov
Alexey Romanov

Reputation: 170723

If more than one member method is both accessible and applicable to a method invocation, it is necessary to choose one to provide the descriptor for the run-time method dispatch. The Java programming language uses the rule that the most specific method is chosen.

This is the same in Java and Scala: which overload is used is chosen at compile-time (and then this specific overload is dispatched at run-time, as the quote says). Since e.left_arg is not known to be either ExpAdd or ExpSub, the compiler can only choose interpret(exp: Exp) and complains when you remove it. One option is to remove the other overloads instead:

def interpret(exp: Exp) = exp match {
  case e : ExpAdd => interpret(e.left_arg) + interpret(e.right_arg)
  case e : ExpSub => interpret(e.left_arg) - interpret(e.right_arg)
}

// when extending
override def interpret(exp: Exp) = exp match {
  case e : ExpMult => interpret(e.left_arg) * interpret(e.right_arg)
  case _ => super.interpret(exp)
}

Upvotes: 1

dimitrisli
dimitrisli

Reputation: 21391

Your definition of Exp and interpret are not fully visible and not sure I understand what you are trying to represent but here's 2 ideas:

  • Try to use operator overloading defining a + and a - methods for more DSL-esque operations
  • Even better so, you can think of the addition, subtraction or any other operation as bahaviours and define them as functions. This way (single data representation with multiple behavioural units) your code will follow more FP principles.

Upvotes: 1

Related Questions