Case classes, pattern matching and varargs

前端 未结 3 669
执笔经年
执笔经年 2020-12-31 10:36

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


        
相关标签:
3条回答
  • 2020-12-31 10:55

    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.

    0 讨论(0)
  • 2020-12-31 10:58

    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*.

    0 讨论(0)
  • 2020-12-31 11:07

    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.

    0 讨论(0)
提交回复
热议问题