Классы Scala часто имеют методы apply и unapply в своем сопутствующем объекте.

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

Чтобы взять конкретный пример, представьте себе класс case Url:

object Url {
  def apply(str: String): Url = ??? 
  def unapply(str: String): Option[Url] = ???
}

case class Url(protocol: String, host: String, path: String)

Если str - действительный URL, то unapply вернет Some[Url], иначе None.

Однако apply для меня немного менее ясен: как он должен реагировать на то, что str не является действительным URL-адресом?

Исходя из мира Java, я первым делом решил бросить IllegalArgumentException, что позволило бы нам реализовать сопутствующий объект как:

object Url {
  def apply(str: String): Url = ... // some function that parses a URI and throws if it fails.

  def unapply(str: String): Option[Url] = Try(apply(str)).toOption
}

Я понимаю, что это не считается очень хорошей практикой в ​​функциональном мире (как объясняется, например, в этот ответ).

Альтернативой было бы, чтобы apply возвращал Option[Url], и в этом случае это был бы простой клон unapply, и лучше оставить его нереализованным.

Это правильный вывод? Следует ли просто не реализовать этот тип потенциально неудачных методов apply? Считается ли бросок в этом случае нормальным? Есть ли третий вариант, который я не вижу?

4
Nicolas Rinaudo 2 Июн 2014 в 16:21

2 ответа

Лучший ответ

Это немного субъективно, но я не думаю, что вам следует делать то же самое.

Предположим, вы разрешили apply завершиться ошибкой, т.е. выбросить исключение или вернуть пустую опцию. Тогда выполнение val url = Url(someString) может потерпеть неудачу, несмотря на то, что он ужасно похож на конструктор. В этом вся проблема: метод apply сопутствующего объекта должен надежно создавать для вас новые экземпляры, а вы просто не можете надежно построить экземпляры Url из произвольных строк. Так что не делай этого.

unapply обычно следует использовать для получения действительного объекта Url и возврата другого представления, с помощью которого вы могли бы снова создать Url. В качестве примера рассмотрим сгенерированный метод unapply для классов case, который просто возвращает кортеж, содержащий аргументы, с которыми он был построен. Так что подпись должна быть def unapply(url: Url): String.

Итак, я пришел к выводу, что ни один не следует использовать для построения Url. Я думаю, что было бы наиболее идиоматично иметь метод def parse(str: String): Option[Url], чтобы явно указать, что вы на самом деле делаете (анализируете строку) и что он может потерпеть неудачу. Затем вы можете сделать Url.parse(someString).map(url => ...), чтобы использовать свой экземпляр Url.

7
DCKing 2 Июн 2014 в 14:06

Обычно ответственность за решение, как справиться с ошибкой в ​​случае None, лежит на пользователе экстрактора, и синтаксис сопоставления с образцом делает это достаточно удобным. Вот немного более простой пример:

import scala.util.Try

object IntString {
  def unapply(s: String) = Try(s.toInt).toOption
}

Теперь мы можем написать любое из следующего:

def intStringEither(s: String): Either[String, Int] = s match {
  case IntString(i) => Right(i)
  case invalid => Left(invalid)
}

Или же:

def intStringOption(s: String): Option[Int] = s match {
  case IntString(i) => Some(i)
  case _ => None
} // ...or equivalently just `= IntString.unapply(s)`.

Или же:

def intStringCustomException(s: String): Int = s match {
  case IntString(i) => i
  case invalid => throw MyCustomParseFailure(invalid)
}

Эта гибкость - одно из преимуществ неэкстракторов, и если вы выбрасываете (нефатальные) исключения в unapply, вы сокращаете эту гибкость.

3
Travis Brown 2 Июн 2014 в 12:37