Oto, w jaki sposób robi to Haskell: (nie do końca sprzeczne z twierdzeniami Lipperta, ponieważ Haskell nie jest językiem zorientowanym obiektowo).
OSTRZEŻENIE: długa, wyczerpująca odpowiedź od poważnego fan-fan Haskella.
TL; DR
Ten przykład pokazuje dokładnie, jak różni się Haskell od C #. Zamiast delegować logistykę budowy konstrukcji do konstruktora, należy ją obsłużyć w otaczającym kodzie. Nie ma możliwości, aby wartość zerowa (lub Nothing
w Haskell) pojawiła się w miejscu, w którym oczekujemy wartości innej niż zerowa, ponieważ wartości zerowe mogą występować tylko w ramach wywoływanych specjalnych typów opakowań, Maybe
których nie można zamieniać z / bezpośrednio zamieniającymi na zwykłe, inne niż typy zerowalne. Aby użyć wartości, która stała się zerowalna przez zawinięcie jej w a Maybe
, musimy najpierw wyodrębnić wartość za pomocą dopasowania wzorca, co zmusza nas do przekierowania przepływu sterowania do gałęzi, w której wiemy na pewno, że mamy wartość inną niż null.
W związku z tym:
czy zawsze możemy wiedzieć, że odwołanie, które nie ma wartości zerowej, nigdy nie jest w żadnym wypadku uważane za nieprawidłowe?
Tak. Int
i Maybe Int
są dwoma całkowicie oddzielnymi typami. Znalezienie Nothing
w równinie Int
byłoby porównywalne do znalezienia ciągu „ryba” w Int32
.
A co z konstruktorem obiektu o niepisającym polu typu referencyjnego?
To nie problem: konstruktory wartości w Haskell nie mogą nic zrobić, tylko wziąć podane im wartości i złożyć je w całość. Cała logika inicjalizacji ma miejsce przed wywołaniem konstruktora.
A co z finalizatorem takiego obiektu, w którym obiekt jest finalizowany, ponieważ kod, który miał wypełnić referencję, zwrócił wyjątek?
W Haskell nie ma finalistów, więc tak naprawdę nie mogę się tym zająć. Jednak moja pierwsza odpowiedź wciąż trwa.
Pełna odpowiedź :
Haskell nie ma wartości null i używa Maybe
typu danych do reprezentowania wartości zerowych. Może zdefiniowano typ danych algabrycznych w następujący sposób:
data Maybe a = Just a | Nothing
Dla tych z was, którzy nie znają Haskella, przeczytaj to jako „A Maybe
jest albo a, Nothing
albo a Just a
”. Konkretnie:
Maybe
jest konstruktorem typu : można go traktować (niepoprawnie) jako klasę ogólną (gdzie a
jest zmienna typu). Analogia C # jest class Maybe<a>{}
.
Just
jest konstruktorem wartości : jest to funkcja, która pobiera jeden argument typu a
i zwraca wartość typu, Maybe a
która zawiera wartość. Więc kod x = Just 17
jest analogiczny do int? x = 17;
.
Nothing
jest innym konstruktorem wartości, ale nie przyjmuje żadnych argumentów, a Maybe
zwracana wartość nie jest inna niż „Nic”. x = Nothing
jest analogiczny do int? x = null;
(zakładając, że ograniczyliśmy się a
w Haskell Int
, co można zrobić pisząc x = Nothing :: Maybe Int
).
Teraz, gdy podstawy tego Maybe
typu nie są na przeszkodzie, w jaki sposób Haskell unika problemów omawianych w pytaniu PO?
Haskell naprawdę różni się od większości omawianych dotychczas języków, więc zacznę od wyjaśnienia kilku podstawowych zasad językowych.
Po pierwsze, w Haskell wszystko jest niezmienne . Wszystko. Nazwy odnoszą się do wartości, a nie do miejsc w pamięci, w których można przechowywać wartości (samo to jest ogromnym źródłem eliminacji błędów). W przeciwieństwie do C #, gdzie deklaracja zmiennej i przypisanie to dwie odrębne operacje, w wartościach Haskell są tworzone poprzez określenie ich wartości (np x = 15
, y = "quux"
, z = Nothing
), co może nigdy się nie zmieniają. Dlatego kod taki jak:
ReferenceType x;
W Haskell nie jest to możliwe. Nie ma problemów z inicjowaniem wartości, null
ponieważ wszystko musi zostać jawnie zainicjowane na wartość, aby mogła istnieć.
Po drugie, Haskell nie jest językiem zorientowanym obiektowo : jest to język czysto funkcjonalny , więc nie ma obiektów w ścisłym tego słowa znaczeniu. Zamiast tego istnieją po prostu funkcje (konstruktory wartości), które pobierają swoje argumenty i zwracają połączoną strukturę.
Następnie absolutnie nie ma kodu stylu imperatywnego. Rozumiem przez to, że większość języków ma podobny wzór:
do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
do thing 6
return thing 7
Zachowanie programu jest wyrażone jako seria instrukcji. W językach zorientowanych obiektowo deklaracje klas i funkcji również odgrywają ogromną rolę w przepływie programu, ale w istocie „mięso” wykonania programu przybiera formę szeregu instrukcji do wykonania.
W Haskell nie jest to możliwe. Zamiast tego przebieg programu jest podyktowany wyłącznie funkcjami łańcuchowymi. Nawet do
uwaga wyglądająca na konieczną jest po prostu cukrem syntaktycznym służącym do przekazywania >>=
operatorowi anonimowych funkcji . Wszystkie funkcje mają postać:
<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression
Gdzie body-expression
może być coś, co daje wartość. Oczywiście dostępnych jest więcej funkcji składniowych, ale głównym punktem jest całkowity brak sekwencji instrukcji.
Wreszcie, i chyba najważniejsze, system typowania Haskella jest niezwykle surowy. Gdybym musiał podsumować centralną filozofię projektowania systemu typów Haskell, powiedziałbym: „Spraw, aby jak najwięcej rzeczy popsuło się w czasie kompilacji, tak aby jak najmniej popsuła się w czasie wykonywania”. Nie ma żadnych ukrytych konwersji (chcesz awansować Int
do Double
? Użyj fromIntegral
funkcji). Jedyne, co może mieć niepoprawną wartość występującą w czasie wykonywania, to użycie Prelude.undefined
(które najwyraźniej musi tam być i nie można go usunąć ).
Mając to na uwadze, spójrzmy na „zepsuty” przykład amona i spróbuj ponownie wyrazić ten kod w Haskell. Po pierwsze, deklaracja danych (przy użyciu składni rekordu dla nazwanych pól):
data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar }
( foo
i bar
tak naprawdę są tutaj funkcjami dostępowymi do anonimowych pól zamiast rzeczywistych pól, ale możemy zignorować ten szczegół).
Konstruktor NotSoBroken
wartości nie jest w stanie podjąć żadnych działań innych niż wykonanie a Foo
i a Bar
(które nie mają wartości zerowej) i wykonanie NotSoBroken
z nich. Nie ma miejsca, aby umieścić kod rozkazujący, a nawet ręcznie przypisać pola. Cała logika inicjalizacji musi mieć miejsce gdzie indziej, najprawdopodobniej w dedykowanej funkcji fabrycznej.
W tym przykładzie konstrukcja Broken
zawsze kończy się niepowodzeniem. Nie ma sposobu na złamanie NotSoBroken
konstruktora wartości w podobny sposób (po prostu nie ma gdzie napisać kodu), ale możemy stworzyć funkcję fabryczną, która jest podobnie wadliwa.
makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing
(pierwsza linia jest deklaracją podpis typ: makeNotSoBroken
trwa Foo
i Bar
jako argumenty i produkuje Maybe NotSoBroken
).
Typem zwrotnym musi być, Maybe NotSoBroken
a nie tylko NotSoBroken
dlatego, że powiedzieliśmy mu, aby to oszacował Nothing
, co jest konstruktorem wartości Maybe
. Typy po prostu nie pasowałyby do siebie, gdybyśmy napisali coś innego.
Poza tym, że jest absolutnie bezcelowa, ta funkcja nawet nie spełnia swojego prawdziwego celu, co zobaczymy, kiedy będziemy próbować jej użyć. Utwórzmy funkcję o nazwie, useNotSoBroken
która oczekuje NotSoBroken
jako argument:
useNotSoBroken :: NotSoBroken -> Whatever
( useNotSoBroken
przyjmuje NotSoBroken
jako argument i tworzy a Whatever
).
I użyj go w ten sposób:
useNotSoBroken (makeNotSoBroken)
W większości języków takie zachowanie może powodować wyjątek wskaźnika zerowego. W Haskell typy nie pasują do siebie: makeNotSoBroken
zwraca a Maybe NotSoBroken
, ale useNotSoBroken
oczekuje NotSoBroken
. Te typy nie są zamienne, a kod się nie kompiluje.
Aby obejść ten problem, możemy użyć case
instrukcji do rozgałęzienia na podstawie struktury Maybe
wartości (za pomocą funkcji zwanej dopasowaniem wzorca ):
case makeNotSoBroken of
Nothing -> --handle situation here
(Just x) -> useNotSoBroken x
Oczywiście ten fragment kodu musi zostać umieszczony w jakimś kontekście, aby go skompilować, ale pokazuje podstawy, w jaki sposób Haskell obsługuje wartości null. Oto wyjaśnienie powyższego kodu krok po kroku:
- Najpierw
makeNotSoBroken
jest oceniany, co gwarantuje uzyskanie wartości typu Maybe NotSoBroken
.
case
Oświadczenie kontroluje strukturę tej wartości.
- Jeśli wartość jest równa
Nothing
, analizowany jest kod „obsłuż tutaj sytuację”.
- Jeśli zamiast tego wartość pasuje do
Just
wartości, druga gałąź jest wykonywana. Zwróć uwagę, jak klauzula dopasowania jednocześnie identyfikuje wartość jako Just
konstrukcję i wiąże swoje NotSoBroken
pole wewnętrzne z nazwą (w tym przypadku x
). x
mogą być następnie używane jak normalna NotSoBroken
wartość.
Tak więc dopasowanie wzorca zapewnia potężne narzędzie do egzekwowania bezpieczeństwa typu, ponieważ struktura obiektu jest nierozerwalnie związana z rozgałęzieniem kontroli.
Mam nadzieję, że to zrozumiałe wytłumaczenie. Jeśli to nie ma sensu, wskocz do Learn You A Haskell For Great Good! , jeden z najlepszych samouczków językowych online, jakie kiedykolwiek czytałem. Mam nadzieję, że zobaczysz to samo piękno w tym języku, co ja.