Примеры классов, сопоставления образцов и varargs

Скажем, у меня есть такая иерархия классов:

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

Было бы лучше определить конструктор ExpList следующим образом:

case class ExpList(listExp: Expr*) extends Expr

Я хотел бы знать, каковы недостатки/преимущества каждого определения в отношении соответствия шаблонов?

Ответ 1

У вас могут быть оба конструктора:

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 преобразуются в mutable.WrappedArray, поэтому, чтобы поддерживать неизменность условного класса case, вы должны использовать список как фактическое значение.

Ответ 2

Позвольте ответить на различные вопросы, которые здесь обсуждаются. Я бы действительно рекомендовал этот синтаксис:

case class ExpList(listExp: Expr*) extends Expr

Но ответ зависит от вашего примера кодирования. Поэтому давайте посмотрим, как использовать varargs в сопоставлении с образцом, когда использовать List и проблему с WrappedArray. Небольшое замечание: несоответствие между Expr и ExpList (с или без 'r'?) Проблематично при наборе текста и попытке вспомнить, что есть - придерживаться одного соглашения, Exp достаточно ясно и часто используется.

Варвар и сопоставление образцов

Давайте сначала рассмотрим это выражение:

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

И этот пример кода:

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

дает:

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

Что я здесь использую: ExpList(a, b) соответствует ExpList с двумя детьми; ExpList(a) соответствует ExpList с одним дочерним элементом. _* - это шаблон, который соответствует последовательностям значений типа A, которые могут быть сколь угодно длинными (включая 0). Я также использую привязки шаблонов, identifier @ pattern, которые позволяют связывать объект, а также дополнительно разрушать его другим шаблоном; они работают с любым шаблоном, а не только с _*.

При использовании identifier @ _* identifier привязывается к типу Seq[A].

Все эти конструкции также применимы к Seq; но если мы используем Seq в объявлении, например:

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

те же предложения case меняются от (например) case ExpList(a, b, c, d @ _*) => до case ExpList(Seq(a, b, c, d @ _*)) =>. Таким образом, более синтаксический беспорядок.

Синтаксически говоря, единственное, что "сложнее" с Expr* заключается в написании следующей функции, которая строит ExpList из списка выражений:

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

Обратите внимание на использование (снова) _* здесь.

Класс List

List удобно использовать, когда вы сопоставляете шаблон с конструктором списка, как в xs match { case head :: tail => ... case Nil => }. Однако обычно этот код можно выразить более компактно, используя складки, и если вы не пишете код в этом стиле, вам не нужно использовать List. Особенно в интерфейсе часто бывает, что требуется только то, что понадобится вашему коду.

Изменчивость

То, о чем мы говорили выше, касалось неизменности. Экземпляры классов дел должны быть неизменными. Теперь при использовании Expr* параметр класса case фактически имеет тип collection.Seq[Expr], и этот тип содержит изменяемые экземпляры - фактически, ExprList получит экземпляр подкласса WrappedArray, который является изменяемым. Обратите внимание, что collection.Seq является суперклассом как collection.mutable.Seq, так и collection.immutable.Seq, а последний по умолчанию имеет псевдоним Seq.

Нельзя мутировать такое значение, не опуская его, но это возможно для кого-то (я не знаю, по какой причине).

Если вы хотите, чтобы ваш клиент не выполнял его, вам нужно преобразовать полученное значение в неизменяемую последовательность, но вы не можете сделать это при объявлении ExpList с помощью case class ExpList(listExp: Expr*) extends Expr.

Вам нужно использовать другой конструктор. Чтобы сделать преобразование в другом коде, так как toSeq возвращает исходную последовательность, вы должны вызвать конструктор Seq с содержимым списка как переменные аргументы. Следовательно, вы используете синтаксис, показанный выше, Seq(listExpr: _*). В настоящее время это не так важно, поскольку Seq реализация по умолчанию - List, но это может измениться в будущем (может быть, что-то быстрее, кто знает?).

Проблемы с стиранием

Нельзя объявить две перегрузки одного и того же метода: один принимает T* и принимает Seq[T], потому что в выходном классе они станут одинаковыми. Небольшой трюк, чтобы заставить m выглядеть по-другому и использовать два конструктора:

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

Здесь я также преобразую массив в неизменяемую последовательность, как указано выше. Соответствие шаблонов выполняется, к сожалению, как в примере выше, где класс case принимает Seq[Expr] вместо Expr*.

Ответ 3

Как комментарий к решению Dan: если у вас есть это внутри функции, которую он делает, из-за ошибки в Scala не работает https://issues.scala-lang.org/browse/SI-3772. Вы получаете что-то вроде:

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 {
                           ^

В настоящее время обходным путем является просто сначала поставить объект.

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

Я надеюсь, что это не позволит людям споткнуться об этой ошибке, как и я.