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

Я не могу понять суть Option[T] класс в Скале. Я имею в виду, я не вижу никаких преимуществ 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?

ОБНОВИТЬ:

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

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

18 ответов

Решение

Вы получите точку 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] над T это тип безопасности. Это гарантирует, что вы не будете отправлять T Метод объекта, который может не существовать, так как компилятор не позволит вам.

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

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

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

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

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

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

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

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

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 как функция карты, позволяет нам связывать операции над объектами, не заботясь о том, являются ли они "нулевыми" или нет.

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

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

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

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

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

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

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

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

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

У вас есть довольно мощные возможности композиции с 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") )

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

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

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

Похоже, что здесь никто не поднял вопрос, что, хотя у вас может быть нулевая ссылка, в 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 либо, если бы не ошибка Хоара в миллиард долларов.

[скопировано из этого комментария 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

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

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

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

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

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

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

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

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

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

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

Как вы продемонстрировали, Option может сделать одну вещь - эмулировать null; Затем вы должны проверить исключительное значение "None" вместо необычного значения "NULL". Если вы забудете, в любом случае произойдут плохие вещи. Опция делает его менее вероятным при случайном использовании, так как вам нужно набрать "get" (что должно напомнить вам, что оно может быть нулевым, я имею в виду 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 в один удобный оператор, который извлекает именно то значение, которое вы хотите.

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

http://functionaljava.org/examples/

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

На самом деле я разделяю сомнение с вами. Что касается 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)

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

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

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

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

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

Другие вопросы по тегам