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 Nothingw 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ń, Maybektó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. Inti Maybe Intsą dwoma całkowicie oddzielnymi typami. Znalezienie Nothingw równinie Intbył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 Maybetypu 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 Maybejest albo a, Nothingalbo a Just a”. Konkretnie:
Maybejest konstruktorem typu : można go traktować (niepoprawnie) jako klasę ogólną (gdzie ajest zmienna typu). Analogia C # jest class Maybe<a>{}.
Justjest konstruktorem wartości : jest to funkcja, która pobiera jeden argument typu ai zwraca wartość typu, Maybe aktóra zawiera wartość. Więc kod x = Just 17jest analogiczny do int? x = 17;.
Nothingjest innym konstruktorem wartości, ale nie przyjmuje żadnych argumentów, a Maybezwracana wartość nie jest inna niż „Nic”. x = Nothingjest analogiczny do int? x = null;(zakładając, że ograniczyliśmy się aw Haskell Int, co można zrobić pisząc x = Nothing :: Maybe Int).
Teraz, gdy podstawy tego Maybetypu 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, nullponieważ 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 douwaga 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-expressionmoż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ć Intdo Double? Użyj fromIntegralfunkcji). 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 }
( fooi bartak naprawdę są tutaj funkcjami dostępowymi do anonimowych pól zamiast rzeczywistych pól, ale możemy zignorować ten szczegół).
Konstruktor NotSoBrokenwartości nie jest w stanie podjąć żadnych działań innych niż wykonanie a Fooi a Bar(które nie mają wartości zerowej) i wykonanie NotSoBrokenz 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 Brokenzawsze kończy się niepowodzeniem. Nie ma sposobu na złamanie NotSoBrokenkonstruktora 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: makeNotSoBrokentrwa Fooi Barjako argumenty i produkuje Maybe NotSoBroken).
Typem zwrotnym musi być, Maybe NotSoBrokena nie tylko NotSoBrokendlatego, ż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, useNotSoBrokenktóra oczekuje NotSoBrokenjako argument:
useNotSoBroken :: NotSoBroken -> Whatever
( useNotSoBrokenprzyjmuje NotSoBrokenjako 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: makeNotSoBrokenzwraca a Maybe NotSoBroken, ale useNotSoBrokenoczekuje NotSoBroken. Te typy nie są zamienne, a kod się nie kompiluje.
Aby obejść ten problem, możemy użyć caseinstrukcji do rozgałęzienia na podstawie struktury Maybewartoś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
makeNotSoBrokenjest oceniany, co gwarantuje uzyskanie wartości typu Maybe NotSoBroken.
caseOś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
Justwartości, druga gałąź jest wykonywana. Zwróć uwagę, jak klauzula dopasowania jednocześnie identyfikuje wartość jako Justkonstrukcję i wiąże swoje NotSoBrokenpole wewnętrzne z nazwą (w tym przypadku x). xmogą być następnie używane jak normalna NotSoBrokenwartość.
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.