Расширение соответствия шаблонов Scala в подклассе при сохранении сложности - PullRequest
2 голосов
/ 08 февраля 2012

У меня вопрос по Scala.Представьте, что вы создаете код для обработки различных операций, например

operation match {
   case A => doA()
   case B => doB()
   case _ => throw new Exception("Unknown op: " + operation)
}

Теперь представьте, что в дальнейшем вы захотите создать новую версию и захотите расширить соответствие этого шаблона для операции C. Как вы можете это сделать?таким образом, что разрешение операции все еще равно O (1)?

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

   case _ => handleUnknownOperation(operation)

И подкласс мог бы реализовать handleUnknownOperation, чтобы сделать:

operation match {
   case C => doC()
}

Но это отстой, потому что это означает, что для операции C требуется O (2).

Какие-нибудь другие идеи или лучшие практики для расширения такого типа структур сопоставления с образцом?

Приветствия, Галдер

Ответы [ 3 ]

4 голосов
/ 08 февраля 2012

Чтобы ответить на исходный вопрос, сопоставление с образцом эффективно переводится в серию операторов if / else, просто в серию тестов. case _ в конце - просто прорыв (без связанного теста). Таким образом, существует очень небольшая разница между наличием A, B и C в одном совпадении и A и B в совпадении, а затем делегированием в другое совпадение, которое соответствует C. Используя следующее в качестве примера:

class T

case class A(v: Int) extends T
case class B(v: Int) extends T
case class C(v: Int) extends T

class Foo {
  def getOperation(t: T): Unit = {
    t match {
      case A(2) => println("A")
      case B(i) => println("B")
      case _ => unknownOperation(t)
    }
  }

  def unknownOperation(t: T): Unit = println("unknown operation t=" + t)
}

class Bar extends Foo {
  override def unknownOperation(t: T): Unit = t match {
    case C(i) => println("C")
    case _ => println("unknown operation t=" + t)
  }
}

Используя jad для декомпиляции Foo.class, мы получаем:

public void getOperation(T t) {
label0:
    {
        T t1 = t;
        if(t1 instanceof A)
        {
            if(((A)t1).v() == 2)
            {
                Predef$.MODULE$.println("A");
                break label0;
            }
        } else
        if(t1 instanceof B)
        {
            Predef$.MODULE$.println("B");
            break label0;
        }
        unknownOperation(t);
    }
}

Итак, я бы сказал, что вам не стоит беспокоиться о производительности [*].

Однако, с точки зрения дизайна, я бы, вероятно, немного поменял вещи и либо использовал шаблон Command, как предлагает Фрэнк, либо вместо переопределения unknownOperation в подклассе, вы могли бы переопределить getOperation, что делает совпадение с C и затем делегирование на super.getOperation(), что мне кажется более точным.

class Bar extends Foo {
  override def getOperation(t: T): Unit = t match {
    case C(i) => println("C")
    case _ => super.getOperation(t)
  }
}

[*] Предостережение для этого будет сложностью. Существует проблема в версиях Scala до 2.10, в которых средство сопоставления с образцами генерирует слишком сложные файлы .class ( вложенные экстракторы генерируют байт-код экспоненциального пространства ), поэтому, если вы используете совпадение, которое очень сложный, это может вызвать проблемы. Это исправлено в 2.10 с помощью виртуального сопоставителя шаблонов. Если у вас возникла эта проблема, то одним из обходных путей для этой ошибки является разделение совпадений шаблонов на разные методы / классы.

2 голосов
/ 08 февраля 2012

re: метод handleUnknownOperation, это не очень чистый ОО дизайн.Например, это становится немного странным, когда вы хотите обработать операцию D, если подкласс v3.Вместо этого просто создайте метод handle, который могут переопределять подклассы:

class BaseProtocol {
  def handle(operation: Operation) = operation match {
    case A => doA()
    case B => doB()
    case _ => throw new Exception("Unknown op: " + operation)
  }
}

class DerivedProtocol extends BaseProtocol {
  override def handle(operation: Operation) = operation match {
    case C => doC()
    case _ => super.handle(operation)
  }
}

re: эффективность, вы, вероятно, преждевременно оптимизируете, но я не позволю этому остановить меня: -)

Все ваши operation с object с?Если это так, вы можете заменить операторы match на Map.

class BaseProtocol {
  def handlers = Map(A -> doA _, B -> doB _)

  def handle(operation: Operation) =
    handlers.get(operation)
      .getOrElse(throw new Exception("Unknown op: " + operation))
      .apply()
}

class DerivedProtocol extends BaseProtocol {
  override def handlers = super.handlers + (C -> doC _)
}
0 голосов
/ 08 февраля 2012

В соответствии с моими комментариями выше, я рассматриваю этот вопрос с точки зрения дизайна, а не с точки зрения производительности.

Таким образом, вопрос слегка смещается к: Как вы можете написать реализацию протокола, поддерживающую несколько возможных операций, и сделать его версионным, чтобы можно было настроить поиск операций?

Один из подходов к решению был дан в комментариях с точки зрения подклассов. Чтобы сохранить механизм разрешения операций без изменений, я бы даже предложил заменить сопоставление с шаблоном другим механизмом поиска.

Вот идея:

  • Базовый класс / черта CanPerformOperations. Этот класс хранит хэш-карту, которая отображает операции (похоже, вы используете case object s для этого, что было бы неплохо) для функций, которые выполняют фактическую операцию. Он также предоставляет методы для регистрации операций, то есть изменения карты хеш-функции и выполнения операции, связанной с командой, путем поиска функции операции в карте хеш-функции и ее выполнения.
  • Класс ProtocolVersion1 extends CanPerformOperations регистрирует операции doA и doB для команд A и B соответственно.
  • Класс ProtocolVersion2 extends ProtocolVersion1 дополнительно регистрирует операцию doC для команды C.
  • Класс ProtocolVersion2_1 extends ProtocolVersion2 может зарегистрировать новую операцию для команды A, поскольку поведение в версии 2.1 отличается от поведения в версии 1.0.

По сути, это разделяет механизм поиска и выполнения команд на класс trait / base и дает вам полную гибкость в определении команд, поддерживаемых конкретными реализациями.

Конечно, все это отчасти связано с шаблоном команд .

В принципе, это не ответ на ваш вопрос как таковой, поскольку совпадение не продлено. Практически, он достигает того, что вы хотите сделать, сохраняя (амортизированную) постоянную сложность.

...