В чем смысл класса 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 в обоих случаях.: (

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

Ответы

Ответ 1

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

Итак, возьмите этот пример. Как вы могли бы назвать 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] что он явно сообщает программистом, что этот метод мог бы return None?

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

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

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

[скопирован из этот комментарий от Daniel Spiewak]

Если использовать Option единственный способ сопоставить шаблон, чтобы получить значения, тогда да, я согласен, что это не улучшает вообще значение null. Однако вам не хватает * огромного * класса его функциональности. Единственный убедительной причиной использования 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, и я знаю много других разработчиков Scala, которые находятся в такой же лодка. Дэвид Поллак упомянул я просто на днях, что он использует такое явное совпадение на Option (или Box, в случае Lift) в качестве знака что разработчик, который написал код полностью не понимает язык и его стандартную библиотеку.

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

Ответ 9

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

Ответ 10

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

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

Ответ 11

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

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

Ответ 12

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

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

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

Где Option действительно начинает демонстрировать свою силу, помогает вам разобраться с концепцией I-wanted-something-but-I-don-t-fact-have-one.

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

Возможно, вы хотите установить значение по умолчанию, если у вас есть значение null. Пусть сравниваются Java и Scala:

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

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

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

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

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

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

Ответ 13

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

Ответ 14

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

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

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

Ответ 15

Признавая заранее, что это ответ glib, Option является монадой.

Ответ 16

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

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

Дополнительное преимущество:

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

Невозможное преимущество:

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

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

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

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

Ответ 17

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

Ответ 18

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

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