Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Case classes, pattern matching and varargs

Let's say I have such class hierarchy:

abstract class Expr
case class Var(name: String) extends Expr
case class ExpList(listExp: List[Expr]) extends Expr

Would it be better to define constructor of ExpList like this:

case class ExpList(listExp: Expr*) extends Expr

I would like to know, what are drawbacks/benefits of each definitions regards pattern matching?

like image 814
PrimosK Avatar asked Feb 09 '12 22:02

PrimosK


3 Answers

You can have both constructors:

case class ExpList(listExp: List[Expr]) extends Expr
object ExpList {
  def apply(listExp: Expr*) = new ExpList(listExp.toList)
}

//now you can do
ExpList(List(Var("foo"), Var("bar")))
//or
ExpList(Var("foo"), Var("bar"))

Variadic arguments are converted to a mutable.WrappedArray, so to keep in line with the convention of case classes being immutable, you should use a list as the actual value.

like image 192
Dan Simon Avatar answered Oct 22 '22 01:10

Dan Simon


Let's answer the different questions involved here. I would indeed recommend this syntax:

case class ExpList(listExp: Expr*) extends Expr

But the answer depends on your coding example. So let's see how to use varargs in pattern matching, when to use List, and the problem with WrappedArray. A small remark: the inconsistency between Expr and ExpList (with or without 'r'?) is problematic when typing and trying to remember which is which - stick to one convention, Exp is clear enough and often used.

Varargs and pattern matching

Let us first consider this declaration:

abstract class Expr
case class ExpList(listExp: Expr*) extends Expr
case class Var(name: String) extends Expr

And this code example:

val v = Var("a")
val result = for (i <- Seq(ExpList(v), ExpList(v, v), ExpList(v, v, v))) yield (i match {
  case ExpList(a) => "Length 1 (%s)" format a
  case ExpList(a, b, c, d @ _*) => "Length >= 3 (%s, %s, %s, %s...)" format (a, b, c, d)
  case ExpList(args @ _*) => "Any length: " + args
})
result foreach println

produces:

Length 1 (Var(a))
Any length: WrappedArray(Var(a), Var(a))
Length >= 3 (Var(a), Var(a), Var(a), WrappedArray()...)

What I use here: ExpList(a, b) matches a ExpList with two children; ExpList(a) matches a ExpList with one child. _* is a pattern which matches sequences of values of type A which can be arbitrarily long (including 0). I also use pattern binders, identifier @ pattern, which allow to bind an object while also further destructuring it with another pattern; they work with any pattern, not just _*.

When using identifier @ _*, identifier is bound to type Seq[A].

All these constructs also apply to Seq; but if we do use Seq in the declaration, like this:

case class ExpList(listExp: Seq[Expr]) extends Expr

the same case clauses change from (for instance) case ExpList(a, b, c, d @ _*) => to case ExpList(Seq(a, b, c, d @ _*)) =>. So more syntactic clutter.

Syntactically speaking, the only thing which is 'harder' with Expr* is writing the following function, which constructs a ExpList from an expression list:

def f(x: Seq[Expr]) = ExpList(x: _*)

Note the use (again) of _* here.

The List class

List is convenient to use when you pattern match on the list head constructor, as in xs match { case head :: tail => ... case Nil => }. However, usually this code can be expressed more compactly using folds, and if you are not writing code in this style, you needn't use List. Especially in an interface, it is often good practice to require only what your code is going to need.

Mutability

What we discussed above concerned immutability. Instances of case classes should be immutable. Now, when using Expr*, the parameter of the case class has in fact type collection.Seq[Expr], and this type contains mutable instances - in fact, ExprList will receive an instance of the subclass WrappedArray, which is mutable. Note that collection.Seq is a superclass of both collection.mutable.Seq and collection.immutable.Seq, and the latter is aliased to Seq by default.

One cannot mutate such a value without downcasting it, but it is still possible for somebody to do it (I don't know for what reason).

If you want to prevent your client from doing it, you need to convert the value you receive to an immutable sequence - but you cannot do it when declaring ExpList with case class ExpList(listExp: Expr*) extends Expr.

You need instead to use another constructor. To do the conversion in the other code, since toSeq returns the original sequence, you must call Seq's constructor with the content of the list as variadic arguments. Hence, you use the syntax I showed above, Seq(listExpr: _*). Presently that does not matter so much since Seq's default implementation is List, but that might change in the future (maybe to something faster, who knows?).

Erasure problems

One cannot declare two overloads of the same method, one taking T* and one taking Seq[T], because in the output class they would become the same. A little trick to make the m look different and have two constructors can be used:

case class ExpList(listExp: Seq[Expr]) extends Expr
object ExpList {
 def apply(listExp: Expr*)(implicit d: DummyImplicit) = new ExpList(Seq(listExp: _*))
}

Here I also convert the array to an immutable sequence, as above. Pattern matching is done, unfortunately, as in the example above where the case class accepts Seq[Expr] instead of Expr*.

like image 18
Blaisorblade Avatar answered Oct 22 '22 01:10

Blaisorblade


Just as a comment to Dan's solution: If you have this inside a function it does, due to the bug in Scala not work https://issues.scala-lang.org/browse/SI-3772. You get something like:

scala> :paste
// Entering paste mode (ctrl-D to finish)
    def g(){
        class Expr {}
        case class ExpList(listExp: List[Expr]) extends Expr
        object ExpList {
          def apply(listExp: Expr*) = new ExpList(listExp.toList)
        }
    }
// Exiting paste mode, now interpreting.

<console>:10: error: ExpList is already defined as (compiler-generated) case cla
ss companion object ExpList
                    object ExpList {
                           ^

For now the workaround is simply to put the object first.

scala> :paste
// Entering paste mode (ctrl-D to finish)
    def g(){
        class Expr {}
        object ExpList {
          def apply(listExp: Expr*) = new ExpList(listExp.toList)
        }
        case class ExpList(listExp: List[Expr]) extends Expr
    }

// Exiting paste mode, now interpreting.
g: ()Unit

I hope that will prevent people from stumbling over this bug as I did.

like image 1
leo Avatar answered Oct 22 '22 01:10

leo