Nie jestem w stanie zrozumieć sensu Option[T]
zajęć w Scali. To znaczy, nie jestem w stanie dostrzec żadnych zalet None
ponad null
.
Na przykład rozważ kod:
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 */
}
}
}
Załóżmy teraz, że metoda getPerson1
zwraca null
, a następnie wywołanie display
w pierwszym wierszu programu main
nie powiedzie się NPE
. Podobnie, jeśli getPerson2
zwróci None
, display
wywołanie ponownie zakończy się niepowodzeniem z podobnym błędem.
Jeśli tak, to dlaczego Scala komplikuje sprawę, wprowadzając nowe opakowanie wartości ( Option[T]
) zamiast stosować proste podejście używane w Javie?
AKTUALIZACJA:
Zmodyfikowałem swój kod zgodnie z sugestią @Mitcha . Nadal nie widzę żadnej szczególnej korzyści Option[T]
. Muszę przetestować wyjątkowy null
lub None
w obu przypadkach. :(
Jeśli dobrze zrozumiałem z odpowiedzi @ Michael , czy jedyną zaletą Option[T]
jest to, że wyraźnie informuje ona programistę, że ta metoda może zwrócić None ? Czy to jedyny powód takiego wyboru projektu?
get
, a otrzymasz to. :-)Odpowiedzi:
Osiągniesz cel
Option
lepiej, jeśli zmusisz się, by nigdy, przenigdy nie używaćget
. To dlatego, żeget
jest odpowiednikiem „ok, odeślij mnie z powrotem do null-land”.A więc weź ten swój przykład. Jak byś dzwonił
display
bez używaniaget
? Oto kilka alternatyw:getPerson2 foreach (_.display) for (person <- getPerson2) person.display getPerson2 match { case Some(person) => person.display case _ => } getPerson2.getOrElse(Person("Unknown", 0)).display
Żadna z tych alternatyw nie pozwoli Ci przywołać
display
czegoś, co nie istnieje.Jeśli chodzi o dlaczego
get
istnieje, Scala nie mówi ci, jak powinien być napisany twój kod. Może cię delikatnie szturchnąć, ale jeśli nie chcesz wycofać się z żadnej siatki bezpieczeństwa, to twój wybór.Udało ci się to tutaj:
Z wyjątkiem „jedynego”. Ale pozwólcie, że powtórzę to w inny sposób: główną zaletą
Option[T]
nadwyżkiT
jest bezpieczeństwo typów. Gwarantuje to, że nie wyśleszT
metody do obiektu, który może nie istnieć, ponieważ kompilator ci na to nie pozwoli.Powiedziałeś, że musisz sprawdzić wartość zerową w obu przypadkach, ale jeśli zapomnisz - lub nie wiesz - musisz sprawdzić wartość null, czy kompilator ci powie? A może Twoi użytkownicy?
Oczywiście, ze względu na współdziałanie z Javą, Scala dopuszcza wartości null tak samo jak Java. Więc jeśli używasz bibliotek Java, jeśli używasz źle napisanych bibliotek Scala lub jeśli używasz źle napisanych osobistych bibliotek Scala, nadal będziesz musiał radzić sobie ze wskaźnikami zerowymi.
Inne dwie ważne zalety, które przychodzą
Option
mi do głowy, to:Dokumentacja: podpis typu metody powie ci, czy obiekt jest zawsze zwracany, czy nie.
Komponowalność monadyczna.
Ta ostatnia zajmuje znacznie więcej czasu, aby w pełni docenić i nie nadaje się do prostych przykładów, ponieważ pokazuje swoją siłę tylko w złożonym kodzie. Podam więc przykład poniżej, ale doskonale zdaję sobie sprawę, że to prawie nic nie znaczy, z wyjątkiem ludzi, którzy już to rozumieją.
for { person <- getUsers email <- person.getEmail // Assuming getEmail returns Option[String] } yield (person, email)
źródło
get
” -> Innymi słowy: „Ty nieget
!” :)Porównać:
val p = getPerson1 // a potentially null Person val favouriteColour = if (p == null) p.favouriteColour else null
z:
val p = getPerson2 // an Option[Person] val favouriteColour = p.map(_.favouriteColour)
Monadyczna właściwość bind , która pojawia się w Scali jako funkcja mapy , pozwala nam łączyć operacje na obiektach bez martwienia się o to, czy są one „zerowe”, czy nie.
Weź ten prosty przykład trochę dalej. Powiedzmy, że chcieliśmy znaleźć wszystkie ulubione kolory z listy osób.
// 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
A może chcielibyśmy znaleźć imię siostry matki ojca:
// 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)
Mam nadzieję, że to rzuci trochę światła na to, jak opcje mogą trochę ułatwić życie.
źródło
map
zwróci,None
a wywołanie zakończy się niepowodzeniem z pewnym błędem. Jak to jest lepsze niżnull
podejście?Różnica jest subtelna. Pamiętaj, aby być naprawdę funkcją, musi zwracać wartość - null nie jest tak naprawdę uważane za „normalną wartość zwracaną” w tym sensie, bardziej za typ dolny / nic.
Ale w praktycznym sensie, gdy wywołujesz funkcję, która opcjonalnie zwraca coś, zrobiłbyś:
getPerson2 match { case Some(person) => //handle a person case None => //handle nothing }
To prawda, możesz zrobić coś podobnego z null - ale to sprawia, że semantyka wywoływania jest
getPerson2
oczywista ze względu na fakt, że zwracaOption[Person]
(fajna praktyczna rzecz, inna niż poleganie na kimś, kto przeczyta dokument i uzyska NPE, ponieważ nie czytają doc).Spróbuję znaleźć funkcjonalnego programistę, który może udzielić ściślejszej odpowiedzi niż ja.
źródło
Dla mnie opcje są naprawdę interesujące, gdy są obsługiwane przez składnię ze zrozumieniem. Biorąc synesso poprzedni przykład:
// 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
Jeśli którykolwiek z przydziałów jest
None
,fathersMothersSister
będzie,None
ale nieNullPointerException
zostanie podniesiony. Następnie możesz bezpiecznie przejśćfathersMothersSister
do funkcji pobierającej parametry opcji bez obaw. więc nie sprawdzasz wartości null i nie dbasz o wyjątki. Porównaj to z wersją java przedstawioną w przykładzie synesso .źródło
<-
składnia jest ograniczona do „składni list złożonych”, ponieważ w rzeczywistości jest taka sama, jak bardziej ogólnado
składnia z Haskell lubdomonad
forma z biblioteki monad Clojure. Wiązanie go do list oznacza krótką sprzedaż.Masz całkiem potężne możliwości kompozycji dzięki 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") )
źródło
Może ktoś inny zwrócił na to uwagę, ale ja tego nie widziałem:
Jedną z zalet dopasowywania wzorców z opcją Option [T] w porównaniu ze sprawdzaniem wartości null jest to, że Option jest klasą zapieczętowaną, więc kompilator Scala wyświetli ostrzeżenie, jeśli zaniedbasz kodowanie przypadku Some lub None. W kompilatorze znajduje się flaga kompilatora, która zamienia ostrzeżenia w błędy. Można więc zapobiec niepowodzeniu obsługi przypadku „nie istnieje” w czasie kompilacji, a nie w czasie wykonywania. Jest to ogromna zaleta w porównaniu z użyciem wartości zerowej.
źródło
Nie jest po to, aby pomóc uniknąć sprawdzenia zerowego, ale po to, aby wymusić sprawdzenie zerowe. Sprawa staje się jasna, gdy twoja klasa ma 10 pól, z których dwa mogą być zerowe. Twój system ma 50 innych podobnych klas. W świecie Javy próbujesz zapobiegać NPE na tych polach, używając kombinacji mentalnej władzy, konwencji nazewnictwa, a może nawet adnotacji. A każdy programista Java w znacznym stopniu zawodzi. Klasa Option nie tylko sprawia, że wartości „dopuszczające wartość null” są wizualnie jasne dla wszystkich programistów próbujących zrozumieć kod, ale także umożliwia kompilatorowi wymuszenie tego wcześniej niewypowiedzianego kontraktu.
źródło
[Skopiowane z tego komentarza przez Daniel Śpiewak ]
źródło
Option
naNone
raz. Gdyby stwierdzenia zostały zapisane jako zagnieżdżone warunki warunkowe, każde potencjalne „niepowodzenie” zostałoby przetestowane i zastosowane tylko raz. W twoim przykładzie wynikfetchRowById
jest efektywnie sprawdzany trzy razy: raz w celukey
zainicjowania przewodnika , ponownie dlavalue
's i na koniec dlaresult
' s. To elegancki sposób, aby to napisać, ale nie jest pozbawiony kosztów związanych z uruchomieniem.Jedna kwestia, której nikt inny tutaj nie poruszył, to fakt, że chociaż możesz mieć odniesienie zerowe, istnieje rozróżnienie wprowadzone przez Option.
Czyli można mieć
Option[Option[A]]
, co będzie zamieszkana przezNone
,Some(None)
aSome(Some(a))
gdziea
jest jednym ze zwykłych mieszkańcówA
. Oznacza to, że jeśli masz jakiś kontener i chcesz mieć możliwość przechowywania w nim zerowych wskaźników i pobierania ich, musisz przekazać z powrotem jakąś dodatkową wartość logiczną, aby wiedzieć, czy faktycznie otrzymałeś wartość. Takie brodawki obfitują w API kontenerów java, a niektóre warianty bez blokad nie mogą ich nawet zapewnić.null
jest konstrukcją jednorazową, nie komponuje się ze sobą, jest dostępna tylko dla typów referencyjnych i zmusza do rozumowania w sposób niecałkowity.Na przykład, kiedy sprawdzasz
if (x == null) ... else x.foo()
musisz nosić w głowie po całej
else
gałęzi,x != null
że to już zostało sprawdzone. Jednak przy użyciu czegoś takiego jak opcjax match { case None => ... case Some(y) => y.foo }
wiesz , że nie jest to
None
konstrukcja - i wiedziałbyś, że tak nie byłonull
, gdyby nie pomyłka Hoare'a za miliard dolarów .źródło
Opcja [T] to monada, która jest naprawdę przydatna, gdy używasz funkcji wyższego rzędu do manipulowania wartościami.
Sugeruję przeczytanie artykułów wymienionych poniżej, są to naprawdę dobre artykuły, które pokazują, dlaczego Option [T] jest przydatna i jak można ją wykorzystać w funkcjonalny sposób.
źródło
Dodając do zapowiedzi odpowiedzi Randalla , zrozumienie, dlaczego potencjalny brak wartości jest reprezentowany przez,
Option
wymaga zrozumienia, coOption
dzieli się z wieloma innymi typami w Scali - w szczególności typami modelującymi monady. Jeśli jeden reprezentuje brak wartości z wartością zerową, to rozróżnienie między nieobecnością a obecnością nie może uczestniczyć w kontraktach wspólnych dla innych typów monadycznych.Jeśli nie wiesz, czym są monady lub nie zauważysz, jak są one reprezentowane w bibliotece Scali, nie zobaczysz, z czym
Option
współgrają, i nie możesz zobaczyć, co tracisz. Istnieje wiele korzyści z używaniaOption
zamiast null, który byłby godny uwagi, nawet w przypadku braku jakiejkolwiek koncepcji monada (omówię niektóre z nich w „Koszt opcji / Niektóre vs null” scala-user Mailing List wątek tutaj ), ale mówimy o izolacja przypomina mówienie o typie iteratora konkretnej implementacji listy połączonej, zastanawianie się, dlaczego jest to konieczne, a jednocześnie pomijanie bardziej ogólnego interfejsu kontenera / iteratora / algorytmu. Działa tu też szerszy interfejs,Option
źródło
Myślę, że klucz znajduje się w odpowiedzi Synesso: Opcja nie jest przede wszystkim użyteczna jako uciążliwy alias dla null, ale jako pełnoprawny obiekt, który może pomóc ci w logice.
Problem z wartością null polega na tym, że jest to brak obiektu. Nie ma metod, które mogłyby pomóc ci sobie z tym poradzić (chociaż jako projektant języka możesz dodawać do swojego języka coraz dłuższe listy funkcji, które emulują obiekt, jeśli naprawdę masz na to ochotę).
Jak zademonstrowałeś, jedna rzecz, jaką Option może zrobić, to emulować wartość null; musisz następnie przetestować niezwykłą wartość „Brak” zamiast nadzwyczajnej wartości „null”. Jeśli zapomnisz, w obu przypadkach wydarzy się coś złego. Opcja sprawia, że jest mniej prawdopodobne, że zdarzy się to przez przypadek, ponieważ musisz wpisać „get” (co powinno przypomnieć, że może to być null, eee, mam na myśli None), ale jest to mała korzyść w zamian za dodatkowy obiekt opakowujący .
Tam, gdzie Option naprawdę zaczyna pokazywać swoją moc, pomaga ci radzić sobie z koncepcją-chciałem-coś-ale-tak-nie-mam-jednego.
Rozważmy kilka rzeczy, które możesz chcieć zrobić z rzeczami, które mogą być zerowe.
Może chcesz ustawić wartość domyślną, jeśli masz null. Porównajmy Javę i Scalę:
String s = (input==null) ? "(undefined)" : input; val s = input getOrElse "(undefined)"
Zamiast nieco kłopotliwej konstrukcji?: Mamy metodę, która radzi sobie z ideą „użyj wartości domyślnej, jeśli mam wartość null”. To trochę czyści twój kod.
Może chcesz stworzyć nowy obiekt tylko wtedy, gdy masz prawdziwą wartość. Porównać:
File f = (filename==null) ? null : new File(filename); val f = filename map (new File(_))
Scala jest nieco krótsza i ponownie unika źródeł błędów. Następnie rozważ łączną korzyść, gdy musisz połączyć rzeczy w łańcuch, jak pokazano w przykładach autorstwa Synesso, Daniela i paradygmatu.
Nie jest to duża poprawa, ale jeśli dodasz wszystko, warto wszędzie zapisać kod o bardzo wysokiej wydajności (w którym chcesz uniknąć nawet niewielkiego narzutu związanego z tworzeniem obiektu otoki Some (x)).
Użycie dopasowania nie jest tak naprawdę pomocne samo w sobie, z wyjątkiem jako urządzenie ostrzegające o przypadku null / None. Kiedy jest to naprawdę pomocne, gdy zaczynasz go łączyć, np. Jeśli masz listę opcji:
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.") }
Teraz możesz złożyć wszystkie przypadki None i List-is-empty w jednej poręcznej instrukcji, która wyciąga dokładnie taką wartość, jaką chcesz.
źródło
Zwracane wartości null są obecne tylko w celu zapewnienia zgodności z językiem Java. Nie powinieneś ich używać inaczej.
źródło
To naprawdę kwestia stylu programowania. Używając Funkcjonalnej Javy lub pisząc własne metody pomocnicze, możesz mieć funkcjonalność Option, ale nie rezygnujesz z języka Java:
http://functionaljava.org/examples/#Option.bind
To, że Scala zawiera go domyślnie, nie czyni go wyjątkowym. Większość aspektów języków funkcjonalnych jest dostępna w tej bibliotece i może ona dobrze współistnieć z innym kodem Java. Tak jak możesz wybrać programowanie Scali z wartościami zerowymi, możesz programować Javę bez nich.
źródło
Przyznając z góry, że jest to prosta odpowiedź, Option to monada.
źródło
Właściwie to dzielę z tobą wątpliwości. Jeśli chodzi o Option, naprawdę przeszkadza mi to, że 1) istnieje narzut wydajności, ponieważ istnieje wiele opakowań „Some” tworzonych każdego dnia. 2) W moim kodzie muszę używać dużo Some i Option.
Aby zobaczyć zalety i wady tej decyzji dotyczącej projektowania języka, powinniśmy wziąć pod uwagę alternatywy. Ponieważ Java po prostu ignoruje problem null, nie jest to alternatywa. Rzeczywistą alternatywą jest język programowania Fantom. Istnieją typy dopuszczające wartość null i nie dopuszczające wartości null oraz?. ?: operatory zamiast map / flatMap / getOrElse Scali. W porównaniu widzę następujące punkty:
Zaleta opcji:
Zaleta Nullable:
Więc nie ma tutaj oczywistego zwycięzcy. I jeszcze jedna uwaga. Używanie Option nie ma żadnej głównej korzyści syntaktycznej. Możesz zdefiniować coś takiego:
def nullableMap[T](value: T, f: T => T) = if (value == null) null else f(value)
Lub użyj niektórych niejawnych konwersji, aby uzyskać pritty składnię z kropkami.
źródło
Prawdziwą zaletą posiadania jawnych typów opcji jest to, że nie można ich używać w 98% wszystkich miejsc, a tym samym statycznie wyklucza zerowe wyjątki. (A w pozostałych 2% system typów przypomina ci o prawidłowym sprawdzeniu, kiedy faktycznie uzyskujesz do nich dostęp).
źródło
Inną sytuacją, w której opcja działa, są sytuacje, w których typy nie mogą mieć wartości null. Nie jest możliwe przechowywanie wartości null w wartości Int, Float, Double itp., Ale z Opcją można użyć None.
W Javie trzeba by użyć pudełkowych wersji (Integer, ...) tych typów.
źródło