В чем смысл класса Option [T]?

83

Я не могу понять смысл 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 усложняет ситуацию, вводя новое значение wrapper ( Option[T]) вместо того, чтобы следовать простому подходу, используемому в Java?

ОБНОВИТЬ:

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

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

пропавший
источник
23
На самом деле, метод "get" в Варианте [T] произносится: "Какого черта вы не сопоставляете этот шаблон?"
Митч Блевинс
2
Митч прав. Попробуйте перефразировать свой пример без использования get, и вы его получите . :-)
Дэниел С. Собрал
У вас есть Person p .., который является java .. .try val p = ... Кроме того, есть еще кое-что о Option, как показано ниже Даниэлем и Synesso - здесь есть несколько ОТЛИЧНЫХ ответов.
Майкл Нил
@ Майкл: Ой! Спасибо, что указали; исправил.
missingfaktor
Связанный: stackoverflow.com/questions/3989264/…
Брайан

Ответы:

72

Вы добьетесь 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?

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

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

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

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

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

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

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

for {
  person <- getUsers
  email <- person.getEmail // Assuming getEmail returns Option[String]
} yield (person, email)
Дэниел С. Собрал
источник
5
"заставьте себя никогда, никогда не использовать get" -> Другими словами: "Вы не делаете getэтого!" :)
fredoverflow
31

Сравните:

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)

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

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

// 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)

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

Synesso
источник
В вашем последнем примере что, если отец человека равен нулю? mapвернется, Noneи вызов завершится с ошибкой. Чем это лучше nullподхода?
missingfaktor
5
Нет. Если person - None (или отец, мать или сестра), то FathersMothersSister будет None, но ошибка не будет.
paradigmatic
6
Я думаю, вы имеете в виду flatMap, а не карту.
ретроним
Спасибо за редактирование, Даниэль. Я не пробовал код перед публикацией. Будет лучше в следующий раз.
Synesso
2
val favouriteColour = if (p == null) p.favouriteColour else null // именно та ошибка, которой Option помогает избежать! Этот ответ был здесь в течение многих лет, и никто не заметил эту ошибку!
Марк Листер
22

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

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

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

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

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

Майкл Нил
источник
1
Я тоже так понимаю вариант. Он явно сообщает программисту, что мы можем получить None, и если вы достаточно глупы, чтобы не забыть выполнить Some (T), но не поймать None, у вас проблемы.
cflewis
1
Льюишам - я думаю, что компилятор выдаст вам предупреждение, поскольку Some / None образуют алгебраический тип данных (абстрактная запечатанная черта ...) (но я исхожу из памяти здесь).
Майкл Нил
6
Смысл типа Option на большинстве языков, которые его используют, заключается в том, что вместо нулевого исключения времени выполнения вы получаете ошибку типа времени компиляции - компилятор может знать, что у вас нет действия для условия None при использовании данных, что должно быть ошибкой типа.
Джастин Смит
15

Для меня варианты действительно интересны, когда используются для понимания синтаксиса. Взяв 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, не беспокоясь. поэтому вы не проверяете значение null и не заботитесь об исключениях. Сравните это с версией java, представленной в примере synesso .

парадигматический
источник
3
Жаль, что в Scala <-синтаксис ограничен «синтаксисом понимания списка», так как он действительно такой же, как более общий doсинтаксис из Haskell или domonadформа из библиотеки монад Clojure. Привязка его к спискам ведет к недооценке.
seh
11
«Для понимания» в Scala - это, по сути, «делать» в Haskell, они не ограничиваются списками, вы можете использовать все, что реализует: def map [B] (f: A => B): C [B] def flatMap [B] (f: A => C [B]): C [B] фильтр def (p: A => Boolean): C [A]. IOW, любая монада
GClaramunt
2
@seh Я поддержал комментарий @GClaramunt, но я не могу достаточно подчеркнуть его точку зрения. В Scala нет связи между for-complationions и списками - за исключением того, что последние можно использовать с первыми. Я отсылаю вас к stackoverflow.com/questions/1052476/… .
Дэниел С. Собрал
Да, я знаю, что нет никаких отношений, но я согласен, что на это стоит указать; Я комментировал первую строку этого ответа, где в парадигматике упоминается «синтаксис понимания списка». Это проблема обучения, а не проблема языкового дизайна.
seh
9

У вас есть довольно мощные возможности композиции с помощью 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") )
Виктор Кланг
источник
Не могли бы вы объяснить это полностью?
Jesvin Jose
8

Возможно, кто-то еще указал на это, но я этого не видел:

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

Пол Снивли
источник
7

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

Адам Рабунг
источник
6

[Скопирована из этого комментария от Daniel Spiewak ]

Если бы единственным способом использования 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-comprehensions:

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

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

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

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

пропавший
источник
Там не грустный результат здесь: нет никакого скачка на основе короткого замыкания в игре, так что каждый последующий оператор тестирует Optionдля Noneснова. Если бы операторы были написаны как вложенные условные выражения, каждый потенциальный «сбой» проверялся бы и принимал меры только один раз. В вашем примере результат fetchRowByIdэффективно проверяется три раза: один раз для keyинициализации руководства , снова для values и, наконец, для results. Это элегантный способ его написания, но он не обходится без затрат времени выполнения.
seh
4
Я думаю, вы неправильно понимаете суть понимания в Scala. Второй пример категорически НЕ является циклом, он переводится компилятором в серию операций flatMap - как в первом примере.
Кевин Райт,
Прошло много времени с тех пор, как я писал здесь свой комментарий, но я только что видел комментарий Кевина. Кевин, на кого вы имели в виду, когда написали "вы неправильно поняли?" Я не понимаю, как это мог быть я , потому что я никогда ничего не упоминал о петле.
seh
6

Один момент, который, похоже, никто здесь не поднял, заключается в том, что, хотя вы можете иметь нулевую ссылку, существует различие, введенное 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
}

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

Эдвард КМЕТТ
источник
3

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

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

Брайан Хсу
источник
Я добавлю в список рекомендованной литературы недавно опубликованный учебник Тони Морриса «Что означает монада?»: Projects.tmorris.net/public/what-does-monad-mean/artifacts/1.0/…
Randall Schulz,
3

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

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

seh
источник
Большое спасибо за ссылку. Это было действительно полезно. :)
missingfaktor
Ваш комментарий к теме был настолько кратким, что я почти упустил его суть. Я действительно хочу, чтобы null был забанен.
Ален О'Ди
2

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

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

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

Вариант действительно начинает показывать свою мощь, так это помогает вам разобраться с концепцией «я-чего-то хотел-но-у меня-на самом деле-нет».

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

Возможно, вы захотите установить значение по умолчанию, если у вас есть null. Сравним 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)).

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

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

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

ржаной
источник
1

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

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

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

Сэм Пуллара
источник
0

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

Рэндалл Шульц
источник
Я знаю, что это монада. Зачем еще я мог бы включить рассматриваемый тег «монада»?
missingfaktor
^ Приведенное выше утверждение не означает, что я понимаю, что такое монада. : D
missingfaktor
4
Монады - это круто. Если вы их не используете или, по крайней мере, не делаете вид, что понимаете, тогда вы не круты ;-)
парадигматический
0

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

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

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

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

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

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

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

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

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

Алексей
источник
Кто-нибудь провел надежные тесты производительности современной виртуальной машины? Анализ выхода означает, что многие временные объекты Option могут быть размещены в стеке (намного дешевле, чем куча), а сборщик мусора поколений довольно эффективно обрабатывает немного меньшее количество временных объектов. Конечно, если скорость важнее для вашего проекта, чем отказ от NPE, варианты, вероятно, не для вас.
Justin W
Не упоминайте накладные расходы на производительность без цифр, подтверждающих это. Это чрезвычайно распространенная ошибка, когда спорят против таких абстракций, как Option. Я с радостью отменю свой отрицательный голос, если вы укажете или опубликуете тест или удалите комментарий о производительности :)
Ален О'Ди,
0

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

Андреас Россберг
источник
-3

Другая ситуация, в которой работает Option, - это ситуации, когда типы не могут иметь нулевое значение. Невозможно сохранить null в значениях Int, Float, Double и т. Д., Но с помощью Option вы можете использовать None.

В Java вам нужно будет использовать коробочные версии (Integer, ...) этих типов.

Арне
источник