it-roy-ru.com

Какой смысл в классе Option [T]?

Я не могу понять суть класса Option[T] в Scala. Я имею в виду, я не вижу никаких преимуществ None перед null.

Например, рассмотрим код:

object Main{
  class Person(name: String, var age: int){
    def display = println(name+" "+age)
  }

  def getPerson1: Person = {
    // returns a Person instance or null
  }

  def getPerson2: Option[Person] = {
    // returns either Some[Person] or None
  }

  def main(argv: Array[String]): Unit = {
    val p = getPerson1
    if (p!=null) p.display

    getPerson2 match{
      case Some(person) => person.display
      case None => /* Do nothing */
    }
  }
}

Теперь предположим, что метод getPerson1 возвращает null, а затем вызов display в первой строке main обречен на неудачу с NPE. Аналогично, если getPerson2 возвращает None, вызов display снова завершится ошибкой с некоторой похожей ошибкой.

Если так, то почему Scala усложняет ситуацию, вводя новый упаковщик значений (Option[T]) вместо того, чтобы следовать простому подходу, используемому в Java?

UPDATE:

Я отредактировал свой код в соответствии с предложением @ Mitch . Я до сих пор не вижу особого преимущества Option[T]. Я должен проверить исключительные null или None в обоих случаях. :(

Если я правильно понял из @ ответ Михаила , единственное преимущество Option[T] заключается в том, что он явно сообщает программисту, что этот метод может вернуть None ? Это единственная причина такого выбора дизайна?

82
missingfaktor

Вы получите смысл Option лучше, если вы заставите себя никогда, никогда не использовать get. Это потому, что get является эквивалентом "хорошо, отправьте меня обратно в нуль-страну".

Итак, возьмите этот пример. Как бы вы назвали display без использования get? Вот несколько альтернатив:

getPerson2 foreach (_.display)
for (person <- getPerson2) person.display
getPerson2 match {
  case Some(person) => person.display
  case _ =>
}
getPerson2.getOrElse(Person("Unknown", 0)).display

Ни одна из этих альтернатив не позволит вам вызвать display для чего-то, что не существует.

Что касается того, почему get существует, Scala не говорит вам, как должен быть написан ваш код. Это может мягко подтолкнуть вас, но если вы хотите отказаться от системы безопасности, это ваш выбор.


Вы прибили это здесь:

единственное преимущество Option [T] заключается в том, что он явно сообщает программисту, что этот метод может вернуть None?

За исключением "только". Но позвольте мне повторить это по-другому: преимущество main в Option[T] по сравнению с T - это безопасность типов. Это гарантирует, что вы не будете отправлять метод T для объекта, который может не существовать, так как компилятор не позволит вам.

Вы сказали, что должны проверять на обнуляемость в обоих случаях, но если вы забудете - или не знаете - вы должны проверить на нулевое значение, скажет ли вам компилятор? Или будут ваши пользователи?

Конечно, из-за своей совместимости с Java, Scala допускает нулевые значения так же, как Java. Поэтому, если вы используете библиотеки Java, если вы используете плохо написанные библиотеки Scala, или если вы используете плохо написанные личные Scala библиотеки, вам все равно придется иметь дело с нулевыми указателями.

Два других важных преимущества Option, о которых я могу думать:

  • Документация: подпись типа метода скажет вам, всегда ли возвращается объект или нет.

  • Монадическая композитность.

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

for {
  person <- getUsers
  email <- person.getEmail // Assuming getEmail returns Option[String]
} yield (person, email)
71
Daniel C. Sobral

Для сравнения:

val p = getPerson1 // a potentially null Person
val favouriteColour = if (p == null) p.favouriteColour else null

с:

val p = getPerson2 // an Option[Person]
val favouriteColour = p.map(_.favouriteColour)

Монадическое свойство bind, которое появляется в Scala как функция map, позволяет нам связывать операции над объектами, не беспокоясь о том, являются ли они "нулевыми" или нет.

Возьмите этот простой пример немного дальше. Скажем, мы хотели найти все любимые цвета списка людей.

// list of (potentially null) Persons
for (person <- listOfPeople) yield if (person == null) null else person.favouriteColour

// list of Options[Person]
listOfPeople.map(_.map(_.favouriteColour))
listOfPeople.flatMap(_.map(_.favouriteColour)) // discards all None's

Или, возможно, мы хотели бы найти имя сестры матери отца человека:

// with potential nulls
val father = if (person == null) null else person.father
val mother = if (father == null) null else father.mother
val sister = if (mother == null) null else mother.sister

// with options
val fathersMothersSister = getPerson2.flatMap(_.father).flatMap(_.mother).flatMap(_.sister)

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

31
Synesso

Разница неуловима. Имейте в виду, что для того, чтобы быть действительно функцией, она должна возвращать значение - значение null на самом деле не считается "нормальным возвращаемым значением" в этом смысле, более a нижний тип /ничего.

Но в практическом смысле, когда вы вызываете функцию, которая может что-то вернуть, вы должны сделать:

getPerson2 match {
   case Some(person) => //handle a person
   case None => //handle nothing 
}

Конечно, вы можете сделать что-то похожее с null - но это делает семантику вызова getPerson2 очевидной в силу того факта, что он возвращает Option[Person] (хорошая практическая вещь, за исключением того, что кто-то читает документ и получает NPE, потому что он этого не делает читать документ).

Я постараюсь найти функционального программиста, который может дать более строгий ответ, чем я.

21
Michael Neale

Для меня варианты действительно интересны, когда обрабатываются для понимания синтаксиса. Принимая synesso предыдущий пример:

// with potential nulls
val father = if (person == null) null else person.father
val mother = if (father == null) null else father.mother
val sister = if (mother == null) null else mother.sister

// with options
val fathersMothersSister = for {
                                  father <- person.father
                                  mother <- father.mother
                                  sister <- mother.sister
                               } yield sister

Если какое-либо из назначений None, fathersMothersSister будет None, но никакое NullPointerException не будет возбуждено. Затем вы можете безопасно передавать fathersMothersSister в функцию, принимающую параметры Option, не беспокоясь. так что вы не проверяете на ноль и не заботитесь об исключениях. Сравните это с версией Java, представленной в примере synesso.

15
paradigmatic

У вас есть довольно мощные возможности композиции с Option:

def getURL : Option[URL]
def getDefaultURL : Option[URL]


val (Host,port) = (getURL orElse getDefaultURL).map( url => (url.getHost,url.getPort) ).getOrElse( throw new IllegalStateException("No URL defined") )
9
Viktor Klang

Может быть, кто-то еще указал на это, но я этого не видел:

Одно из преимуществ сопоставления с шаблоном с Option [T] по сравнению с проверкой нуля состоит в том, что Option является запечатанным классом, поэтому компилятор Scala выдаст предупреждение, если вы забудете кодировать вариант Some или None. В компиляторе есть флаг компилятора, который превратит предупреждения в ошибки. Таким образом, можно предотвратить сбой обработки случая "не существует" во время компиляции, а не во время выполнения. Это огромное преимущество перед использованием нулевого значения.

8
Paul Snively

Это не для того, чтобы избежать проверки на ноль, а для принудительной проверки на ноль. Суть становится ясной, когда в вашем классе 10 полей, два из которых могут быть нулевыми. И ваша система имеет 50 других подобных классов. В мире Java вы пытаетесь запретить использование NPE в этих полях, используя некоторую комбинацию умственной мощности, соглашения об именах или даже аннотаций. И каждый Java dev терпит неудачу при этом в значительной степени. Класс Option не только делает "обнуляемые" значения визуально понятными для всех разработчиков, пытающихся понять код, но и позволяет компилятору применять этот ранее невысказанный контракт.

7
Adam Rabung

Похоже, что здесь никто не поднял вопрос, что, хотя у вас может быть нулевая ссылка, в Option есть различие.

То есть вы можете иметь Option[Option[A]], который будет заселен None, Some(None) и Some(Some(a)), где a является одним из обычных обитателей A. Это означает, что если у вас есть какой-то контейнер, и вы хотите иметь возможность хранить в нем нулевые указатели и выводить их, вам нужно вернуть некоторое дополнительное логическое значение, чтобы узнать, действительно ли вы получили значение. Подобных бородавок предостаточно в API-контейнерах Java, и некоторые варианты без блокировок даже не могут их предоставить.

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

Например, когда вы проверяете

if (x == null) ...
else x.foo()

вы должны нести в своей голове всю ветвь else, которая x != null и что это уже проверено. Однако при использовании чего-то подобного

x match {
case None => ...
case Some(y) => y.foo
}

вы знаете y не является Noneby конструкцией - и вы бы знали, что это также не null, если бы не Хоара ошибка в миллиард долларов .

6
Edward KMETT

[скопировано из этот комментарийДаниэль Спивак ]

Если единственным способом использования Option было сопоставление с шаблоном для получения значений, то да, я согласен, что это не улучшится вообще по сравнению с нулем. Однако вам не хватает * огромного * класса его функциональных возможностей. Единственная веская причина использовать Option, если вы используете его служебные функции высшего порядка. По сути, вам нужно использовать его монадическую природу. Например (при условии определенного уровня обрезки API):

val row: Option[Row] = database fetchRowById 42
val key: Option[String] = row flatMap { _ get “port_key” }
val value: Option[MyType] = key flatMap (myMap get)
val result: MyType = value getOrElse defaultValue

Там, не так ли изящно? На самом деле, мы можем сделать намного лучше, если будем использовать for- понимания:

val value = for {
row <- database fetchRowById 42
key <- row get "port_key"
value <- myMap get key
} yield value
val result = value getOrElse defaultValue

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

Я никогда, никогда не делал явного соответствия с Option, и я знаю много других Scala разработчиков, которые находятся в одной лодке. На днях Дэвид Поллак упомянул мне, что он использует такое явное сопоставление с Option (или Box, в случае Lift) как знак того, что разработчик, написавший код, не полностью понимает язык и его стандартную библиотеку.

Я не хочу быть молотом-троллем, но вам действительно нужно посмотреть, как языковые функции * фактически * используются на практике, прежде чем ругать их как бесполезные. Я абсолютно согласен с тем, что Option довольно неуместен, поскольку * вы * использовали его, но вы используете его не так, как было задумано.

6
missingfaktor

В добавление к тизер ответа Рэндалла, понимание того, почему потенциальное отсутствие значения представлено Option, требует понимания того, что Option разделяет со многими другими типами в Scala, в частности, с типами, моделирующими монады. Если один представляет отсутствие значения со значением null, то это различие отсутствия-присутствия не может участвовать в контрактах, совместно используемых другими монадическими типами.

Если вы не знаете, что такое монады, или если вы не замечаете, как они представлены в библиотеке Scala, вы не увидите, с чем играет Option, и не увидите, что вам не хватает , Использование Option вместо нуля дает много преимуществ, которые заслуживают внимания даже при отсутствии какой-либо концепции монады (некоторые из них я обсуждаю в разделе "Стоимость варианта/Некоторые против нуля" scala- пользователь ветка списка рассылки здесь ), но говорить об этой изоляции - все равно что говорить о конкретном типе итератора реализации связанного списка, задаваясь вопросом, почему это необходимо, все время отсутствует в более общем интерфейсе контейнера/итератора/алгоритма. Здесь также работает более широкий интерфейс, и Option предоставляет модель присутствия и отсутствия этого интерфейса.

3
seh

Option [T] - это монада, которая действительно полезна, когда вы используете функции высокого порядка для манипулирования значениями.

Я предлагаю вам прочитать статьи, перечисленные ниже, это действительно хорошие статьи, которые показывают вам, почему Option [T] полезен и как его можно использовать функционально.

3
Brian Hsu

Я думаю, ключ кроется в ответе Synesso: Option не в первую очередь полезен как громоздкий псевдоним для null, но как полноценный объект, который может помочь вам с логикой.

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

Как вы продемонстрировали, одна из опций может эмулировать null; Затем вы должны проверить необычное значение "None" вместо необычного значения "NULL". Если вы забудете, в любом случае произойдут плохие вещи. Опция делает ее менее вероятной случайно, так как вы должны набрать "get" (что должно напомнить вам, что она может быть null, я имею в виду None), но это небольшое преимущество в обмен на дополнительный объект-обертку.

Там, где Option действительно начинает показывать, что его сила помогает вам разобраться с концепцией "я хотел чего-то, но я не хочу на самом деле иметь".

Давайте рассмотрим некоторые вещи, которые вы можете захотеть сделать с вещами, которые могут быть нулевыми.

Может быть, вы хотите установить значение по умолчанию, если у вас есть ноль. Давайте сравним Java и ​​Scala:

String s = (input==null) ? "(undefined)" : input;
val s = input getOrElse "(undefined)"

Вместо несколько громоздкой конструкции?: У нас есть метод, который имеет дело с идеей "использовать значение по умолчанию, если я нулевой". Это немного очищает ваш код.

Может быть, вы хотите создать новый объект, только если у вас есть реальная стоимость. Для сравнения:

File f = (filename==null) ? null : new File(filename);
val f = filename map (new File(_))

Scala немного короче и снова избегает источников ошибок. Затем рассмотрите кумулятивную выгоду, когда вам нужно соединить все вместе, как показано в примерах Synesso, Daniel и Paradigmatic.

Это не обширное улучшение, но если вы все сложите, оно того стоит везде, кроме очень высокопроизводительного кода (где вы хотите избежать даже крошечных накладных расходов на создание Some (x) объект-обертка).

Использование совпадений на самом деле не так уж полезно, кроме как в качестве устройства, предупреждающего вас о нулевом/нулевом случае. Когда это действительно полезно, это когда вы начинаете цепочку, например, если у вас есть список опций:

val a = List(Some("Hi"),None,Some("Bye"));
a match {
  case List(Some(x),_*) => println("We started with " + x)
  case _ => println("Nothing to start with.")
}

Теперь вы можете сложить все случаи None и List-is-empty в один удобный оператор, который извлекает именно то значение, которое вы хотите.

2
Rex Kerr

Нулевые возвращаемые значения присутствуют только для совместимости с Java. Вы не должны использовать их в противном случае.

1
ryeguy

Это действительно вопрос стиля программирования. Используя функциональную Java или написав собственные вспомогательные методы, вы можете использовать функциональность Option, но не отказаться от языка Java:

http://functionaljava.org/examples/#Option.bind

То, что Scala по умолчанию включает его, не делает его особенным. В этой библиотеке доступно большинство аспектов функциональных языков, и она может прекрасно сосуществовать с другим кодом Java. Так же, как вы можете программировать Scala с нулями, вы можете программировать Java без них.

1
Sam Pullara

На самом деле я разделяю сомнение с вами. Что касается Option, меня действительно беспокоит то, что 1) производительность снижается, так как существует множество "некоторых" оболочек, созданных каждый раз. 2) Я должен использовать много Some и Option в моем коде.

Таким образом, чтобы увидеть преимущества и недостатки этого решения о разработке языка, мы должны принять во внимание альтернативы. Поскольку Java просто игнорирует проблему обнуляемости, это не альтернатива. Фактическая альтернатива предоставляет язык программирования Fantom. Там есть обнуляемые и не обнуляемые типы и? ?: операторы вместо карты Scala/flatMap/getOrElse. Я вижу следующие пули в сравнении:

Преимущество варианта:

  1. более простой язык - дополнительные языковые конструкции не требуются
  2. униформа с другими монадическими типами

Преимущество Nullable:

  1. более короткий синтаксис в типичных случаях
  2. лучшая производительность (поскольку вам не нужно создавать новые объекты Option и лямбды для карты, flatMap)

Так что здесь нет очевидного победителя. И еще одна заметка. Нет принципиального синтаксического преимущества использования Option. Вы можете определить что-то вроде:

def nullableMap[T](value: T, f: T => T) = if (value == null) null else f(value)

Или используйте некоторые неявные преобразования, чтобы получить точный синтаксис с точками.

0
Alexey

Реальное преимущество наличия явных типов опций заключается в том, что вы можете не использовать их в 98% всех мест и, таким образом, статически исключать нулевые исключения. (А в остальных 2% система типов напоминает вам проверить правильность, когда вы на самом деле получаете к ним доступ.)

0
Andreas Rossberg

Заранее признав, что это бойкий ответ, Option - это монада.

0
Randall Schulz