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

ОНОВЛЕННЯ:

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

Якщо я правильно зрозумів відповідь @ Michael , єдина перевага Option[T]полягає в тому, що він явно повідомляє програмісту, що цей метод може повернути None ? Чи це єдина причина такого вибору дизайну?


23
Насправді, метод "get" у Option [T] вимовляється: "Чому, блін, ти не відповідає цьому шаблону?"
Mitch Blevins

2
Мітч має рацію. Спробуйте переформулювати свій приклад, не використовуючи get, і ви отримаєте його. :-)
Даніель К. Собрал

У вас є Person p .., що є java .. .try val p = ... Крім того, є більше варіанту, як показано Даніелем та Синессо нижче - деякі ВЕЛИКІ відповіді тут.
Michael Neale

@Michael: На жаль! Дякуємо за вказівку; виправив це.
missingfaktor

Відповіді:


72

Ви зрозумієте сенс Option кращого, якщо змусите себе ніколи, ніколи не використовувати get. Це тому, що getце еквівалент "добре, надішліть мене назад до null-land".

Отже, візьміть цей ваш приклад. Як би ви телефонували 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)

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)

Я сподіваюся, це проливає світло на те, як варіанти можуть трохи полегшити життя.


У вашому останньому прикладі, що робити, якщо батько людини нульовий? mapповернеться, Noneі дзвінок не вдасться з деякою помилкою. Чим це краще, ніж nullпідхід?
missingfaktor

5
Ні. Якщо особа - None (або батько, мати чи сестра), тоді fathersMothersSister буде None, але жодної помилки не буде видано.
парадигматичний

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
Льюїшем - Думаю, компілятор дасть вам попередження, оскільки Деякі / Ніхто утворює алгебраїчний тип даних (абстрактно запечатана риса ...) (але тут я йду з пам'яті).
Michael Neale

6
Сенс типу Option у більшості мов, які його використовують, полягає в тому, що замість винятку null під час виконання ви отримуєте помилку типу компіляції - компілятор може знати, що у вас немає дії щодо умови 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, не турбуючись. отже, ви не перевіряєте наявність нуля та не дбаєте про винятки. Порівняйте це з версією 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] фільтр захисту (p: A => логічний): C [A]. IOW, будь-яка монада
GClaramunt

2
@seh Я підтримав коментар @ GClaramunt, але я не можу підкреслити його думку. У Scala немає зв’язку між сприйняттями та списками - за винятком того, що останні можна використовувати з першими. Я посилаю вас на stackoverflow.com/questions/1052476/… .
Daniel C. Sobral

Так, я знаю, що стосунків немає, але я згоден, що на це варто вказати; Я коментував перший рядок цієї відповіді, де парадигматично згадується "синтаксис розуміння списку". Це проблема викладання, на відміну від проблеми мовного дизайну.
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] проти перевірки нуля є те, що Option є герметичним класом, тому компілятор Scala видасть попередження, якщо ви нехтуєте кодувати випадок Some або None. Для компілятора є прапор компілятора, який перетворить попередження на помилки. Отже, можна запобігти невдалому розгляду справи "не існує" під час компіляції, а не під час виконання. Це величезна перевага перед використанням нульового значення.


7

Це не для того, щоб уникнути нульової перевірки, а для примусової перевірки. Суть стає зрозумілою, коли ваш клас має 10 полів, два з яких можуть бути нульовими. А у вашій системі є ще 50 подібних класів. У світі Java ви намагаєтеся запобігти NPE на цих полях, використовуючи якусь комбінацію розумової сиплий сили, дотримання імен або, можливо, навіть анотацій. І кожен розробник Java зазнає невдач у цьому в значній мірі. Клас Option не тільки робить "обнулювані" значення візуально зрозумілими для всіх розробників, які намагаються зрозуміти код, але дозволяє компілятору забезпечити виконання цього раніше невисловленого контракту.


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-розуміння:

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ініціалізації s, знову value's, і , нарешті, result«с. Це елегантний спосіб написати його, але він не позбавлений витрат на час роботи.
seh

4
Я думаю, ти неправильно розумієш розуміння Scala. Другий приклад - це чітко НЕ цикл, він перекладається компілятором у серію операцій flatMap - згідно з першим прикладом.
Кевін Райт,

Давно я не писав тут свій коментар, але я щойно бачив коментар Кевіна. Кевіне, на кого ти мав на увазі, коли писав "неправильно розумієш?" Я не розумію, як це могло бути я , оскільки я ніколи не згадував нічого про цикл.
seh

6

Один момент, який, здається, ніхто інший тут не піднімав, полягає в тому, що хоча ви можете мати нульове посилання, Option має розрізнення.

Тобто ти можеш мати Option[Option[A]], де мешкав би None, Some(None)і Some(Some(a))де aзнаходиться один із звичних жителів Росії A. Це означає, що якщо у вас є якийсь контейнер, і ви хочете мати можливість зберігати в ньому нульові вказівники та витягувати їх, вам потрібно повернути деяке додаткове логічне значення, щоб знати, чи справді ви отримали значення. Подібні бородавки рясніють API API контейнерів, а деякі варіанти без блокування навіть не можуть їх надати.

null це одноразова конструкція, вона не складається з собою, вона доступна лише для еталонних типів, і змушує вас міркувати не в цілому.

Наприклад, коли ви перевіряєте

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

вам доведеться носити у своїй голові всю elseгілку, x != nullі це вже перевірено. Однак, використовуючи щось на зразок опції

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

ви знаєте, що це не Noneконструкція - і ви б знали, що це nullтеж не було , якби не помилка Хоара в мільярді доларів .


3

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

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


Я додаю до рекомендованого списку для читання нещодавно опублікований підручник Тоні Морріса "Що означає монада?": Projects.tmorris.net/public/what-does-monad-mean/artifacts/1.0/…
Рендалл Шульц,

3

Додавання до Рендалла тизер відповідь , розуміючи , чому потенційний відсутність значення представляється , Optionвимагає розуміння того, щоOption акції з багатьма іншими типами в Scala-конкретно, типи моделювання монади. Якщо один із них представляє відсутність значення з нулем, це розмежування відсутності та присутності не може брати участі в контрактах, спільними для інших монадичних типів.

Якщо ви не знаєте, що таке монади, або якщо не помічаєте, як вони представлені в бібліотеці Scala, ви не побачите, що Optionграє разом, і ви не можете побачити, що ви втрачаєте. Існує багато переваг використання Optionзамість null, що було б варте уваги навіть за відсутності будь-якої концепції монади (деякі з них я обговорюю в "Вартість опціону / Деякі проти null" в потоці списку розсилки користувача Scala тут ), але ми говоримо про ця ізоляція нагадує розмову про тип ітератора зв’язаного списку, задаючись питанням, навіщо це потрібно, при цьому втрачаючи загальний інтерфейс контейнера / ітератора / алгоритму. Тут також працює ширший інтерфейс,Option


Велике спасибі за посилання. Це було справді корисно. :)
missingfaktor

Ваш коментар до теми був настільки стислим, що я ледь не пропустив його суть. Я дуже хочу, щоб null можна було заборонити.
Ален О'Ді

2

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

Проблема нуля полягає в тому, що це відсутність об’єкта. У нього немає методів, які могли б допомогти вам впоратися з цим (хоча, як дизайнер мови, ви можете додавати до своєї мови все довші списки функцій, які імітують об’єкт, якщо вам справді хочеться).

Одне, що ви можете зробити, як ви вже продемонстрували, - це емуляція null; тоді вам доведеться перевірити надзвичайне значення "None" замість надзвичайного значення "null". Якщо ви забудете, в будь-якому випадку трапляться погані речі. Варіант зменшує ймовірність того, що це станеться випадково, оскільки вам потрібно набрати "get" (що має нагадати вам, що це може бути нулем, е, я маю на увазі None), але це невелика вигода в обмін на додатковий об'єкт-обгортку .

Там, де Варіант справді починає демонструвати свою силу, це допомагає вам розібратися з концепцією «я хотів чогось, але насправді не маю».

Давайте розглянемо деякі речі, які ви можете захотіти зробити з речами, які можуть бути нульовими.

Можливо, ви хочете встановити значення за замовчуванням, якщо у вас нуль. Давайте порівняємо 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 трохи коротша і знову уникає джерел помилок. Потім розгляньте сукупну вигоду, коли вам потрібно об’єднати речі, як показано на прикладах Синессо, Даніеля та парадигматики.

Це не величезна поліпшення, але якщо ви додасте все вгору, це добре варто всюди зберегти код дуже високої продуктивності (де ви хочете , щоб уникнути навіть крихітні накладних витрат створення Some (х) оболонку об'єкта).

Використання збігів насправді не є таким корисним самостійно, за винятком того, як пристрій, який сповіщає вас про нульовий / жоден випадок. Коли це дійсно корисно, це коли ви починаєте це ланцюжок, наприклад, якщо у вас є список варіантів:

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

Нульові значення повернення наявні лише для сумісності з Java. Не слід використовувати їх інакше.


1

Це справді питання стилю програмування. Використовуючи функціональну Java або написавши власні допоміжні методи, ви можете мати свою функціональність Option, але не відмовлятися від мови Java:

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

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


0

Заздалегідь зізнавшись, що це глибока відповідь, Option - це монада.


Я знаю, що це монада. Чому ще я міг би включити відповідний тег "monad"?
missingfaktor

^ Наведене вище твердження не означає, що я розумію, що таке монада. : D
missingfaktor

4
Монади круті. Якщо ви ними не користуєтесь або, принаймні, не вдаєте, що розумієте, тоді ви не круті ;-)
парадигматичний

0

Насправді я поділяю з вами сумнів. Щодо Варіанту, мене насправді турбує те, що 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)

Або використовуйте деякі неявні перетворення, щоб отримати синтаксис pritty з крапками.


Хтось робив надійні тести щодо показників продуктивності на сучасній ВМ? Аналіз виходу означає, що багато тимчасових об'єктів Option можна виділити у стек (набагато дешевше, ніж купа), а генераційна система управління обробляє дещо менш тимчасові об'єкти досить ефективно. Звичайно, якщо швидкість для вашого проекту важливіша, аніж уникати NPE, варіанти, ймовірно, не для вас.
Justin W,

Не згадуйте накладні витрати на продуктивність без цифр, щоб підтвердити їх. Це надзвичайно поширена помилка, коли аргументується проти таких абстракцій, як Option. Я з радістю скасую свій голос проти, якщо ви вкажете або опублікуєте контрольний показник або видалите коментар до продуктивності :)
Ален О'Деа,

0

Справжньою перевагою наявності явних типів опцій є те, що ви можете не використовувати їх у 98% усіх місць, і таким чином статично виключати нульові винятки. (А в інших 2% система типу нагадує вам перевірити належним чином, коли ви фактично отримуєте до них доступ.)


-3

Інша ситуація, коли Option працює, полягає в ситуаціях, коли типи не можуть мати нульове значення. Неможливо зберегти значення null у значеннях Int, Float, Double тощо, але за допомогою параметра можна використовувати значення None.

У Java вам потрібно буде використовувати пакувальні версії (Integer, ...) цих типів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.