Co oznacza składnia „Just” w Haskell?

118

Przeszukałem internet w poszukiwaniu rzeczywistego wyjaśnienia tego, co robi to słowo kluczowe. Każdy samouczek Haskell, na który patrzyłem, zaczyna go używać losowo i nigdy nie wyjaśnia, do czego służy (a widziałem wiele).

Oto podstawowy fragment kodu z Real World Haskell, który używa Just. Rozumiem, co robi kod, ale nie rozumiem, jaki jest cel lub funkcja Just.

lend amount balance = let reserve    = 100
                      newBalance = balance - amount
                  in if balance < reserve
                     then Nothing
                     else Just newBalance

Z tego, co zaobserwowałem, jest to związane z Maybepisaniem, ale to prawie wszystko, czego się nauczyłem.

Dobre wyjaśnienie, co Justoznacza, byłoby bardzo mile widziane.

reem
źródło

Odpowiedzi:

211

W rzeczywistości jest to zwykły konstruktor danych, który jest zdefiniowany w Prelude , która jest standardową biblioteką importowaną automatycznie do każdego modułu.

Co może jest strukturalnie

Definicja wygląda mniej więcej tak:

data Maybe a = Just a
             | Nothing

Ta deklaracja definiuje typ, Maybe aktóry jest sparametryzowany przez zmienną typu a, co oznacza po prostu, że można go używać z dowolnym typem zamiast a.

Konstruowanie i niszczenie

Typ ma dwa konstruktory Just ai Nothing. Gdy typ ma wiele konstruktorów, oznacza to, że wartość typu musi zostać skonstruowana za pomocą tylko jednego z możliwych konstruktorów. W przypadku tego typu wartość została utworzona za pośrednictwem Justlub Nothing, nie ma innych (bezbłędnych) możliwości.

Ponieważ Nothingnie ma typu parametru, gdy jest używany jako konstruktor, nazywa stałą wartość, która jest członkiem typu Maybe adla wszystkich typów a. Ale Justkonstruktor ma parametr typu, co oznacza, że ​​używany jako konstruktor zachowuje się jak funkcja od typu ado Maybe a, tj. Ma typa -> Maybe a

Zatem konstruktory typu budują wartość tego typu; drugą stroną jest sytuacja, w której chciałbyś użyć tej wartości, i to jest moment, w którym pojawia się dopasowywanie wzorców. W przeciwieństwie do funkcji konstruktorów można używać w wyrażeniach wiążących wzorce i jest to sposób, w jaki można przeprowadzić analizę przypadków wartości należących do typów z więcej niż jednym konstruktorem.

Aby użyć Maybe awartości w dopasowaniu do wzorca, musisz podać wzorzec dla każdego konstruktora, na przykład:

case maybeVal of
    Nothing   -> "There is nothing!"
    Just val  -> "There is a value, and it is " ++ (show val)

W tym przypadku wyrażenie, pierwszy wzorzec pasowałby, gdyby wartość była Nothing, a drugi byłby dopasowany, gdyby wartość została skonstruowana za pomocą Just. Jeśli druga jest zgodna, wiąże również nazwę valz parametrem, który został przekazany do Justkonstruktora, gdy została skonstruowana wartość, do której pasuje.

Co może oznacza

Może już wiesz, jak to działa; wartości nie mają żadnej magii Maybe, to po prostu zwykły algebraiczny typ danych Haskella (ADT). Ale jest używany dość często, ponieważ skutecznie „podnosi” lub rozszerza typ, taki jak Integerz twojego przykładu, do nowego kontekstu, w którym ma dodatkową wartość ( Nothing), która reprezentuje brak wartości! System typu następnie wymaga sprawdzenia tej dodatkowej wartości, zanim będzie pozwalają uzyskać u Integer, że może tam być. Zapobiega to znacznej liczbie błędów.

Obecnie wiele języków obsługuje tego rodzaju wartości „bez wartości” za pośrednictwem odwołań NULL. Tony Hoare, wybitny informatyk (wynalazł Quicksort i jest laureatem nagrody Turinga), przyznaje się do tego jako „miliardowy błąd” . Typ Może nie jest jedynym sposobem, aby to naprawić, ale okazał się skutecznym sposobem na zrobienie tego.

Może jako funktor

Pomysł przekształcenia jednego typu na inny, tak aby operacje na starym typie można było również przekształcić, aby działały na nowym typie, jest koncepcją wywoływaną przez klasę typu Haskell Functor, która Maybe ama przydatne wystąpienie.

Functorudostępnia metodę wywoływaną fmap, która mapuje funkcje, których zakres obejmuje wartości od typu podstawowego (na przykład Integer) do funkcji, których zakres obejmuje wartości z typu zniesionego (na przykład Maybe Integer). Funkcja przekształcona w fmapcelu pracy z Maybewartością działa w ten sposób:

case maybeVal of
  Nothing  -> Nothing         -- there is nothing, so just return Nothing
  Just val -> Just (f val)    -- there is a value, so apply the function to it

Więc jeśli masz Maybe Integerwartość m_xi Int -> Intfunkcję f, możesz fmap f m_xzastosować funkcję fbezpośrednio do funkcji Maybe Integerbez martwienia się, czy rzeczywiście ma wartość, czy nie. W rzeczywistości możesz zastosować cały łańcuch podniesionych Integer -> Integerfunkcji do Maybe Integerwartości i musisz się martwić tylko o jawne sprawdzenie Nothingraz, kiedy skończysz.

Może jako monada

Nie jestem pewien, jak dobrze znasz pojęcie a Monad, ale przynajmniej używałeś go IO awcześniej, a podpis typu IO awygląda niezwykle podobnie do Maybe a. Chociaż IOjest wyjątkowy, ponieważ nie ujawnia swoich konstruktorów przed tobą i dlatego może być "uruchamiany" tylko przez system wykonawczy Haskell, nadal jest również Functordodatkiem do bycia Monad. W rzeczywistości istnieje ważny sens, w którym a Monadjest po prostu specjalnym rodzajem Functorz kilkoma dodatkowymi funkcjami, ale to nie jest miejsce, aby się tym zająć.

W każdym razie, monady lubią IOodwzorowywać typy na nowe typy, które reprezentują „obliczenia, które dają w wyniku wartości” i możesz podnieść funkcje do Monadtypów za pomocą bardzo fmappodobnej funkcji zwanej, liftMktóra zamienia zwykłą funkcję w „obliczenie, którego wynikiem jest wartość uzyskana przez oszacowanie funkcjonować."

Prawdopodobnie zgadłeś (jeśli przeczytałeś tak daleko), że Maybejest to również plik Monad. Reprezentuje „obliczenia, które mogą nie zwrócić wartości”. Podobnie jak w fmapprzykładzie, pozwala to wykonać całą masę obliczeń bez konieczności jawnego sprawdzania błędów po każdym kroku. I faktycznie, sposób, w jaki Monadinstancja jest skonstruowana, obliczanie Maybewartości zatrzymuje się, gdy tylko Nothingzostanie napotkane, więc jest to trochę jak natychmiastowe przerwanie lub bezwartościowy zwrot w środku obliczenia.

Mogłeś napisać może

Jak powiedziałem wcześniej, nie ma nic nieodłącznego od Maybetypu, który jest wbudowany w składnię języka lub system wykonawczy. Jeśli Haskell nie zapewniłby go domyślnie, możesz sam zapewnić całą jego funkcjonalność! W rzeczywistości i tak możesz napisać go ponownie, pod różnymi nazwami i uzyskać tę samą funkcjonalność.

Mamy nadzieję, że rozumiesz teraz Maybetyp i jego konstruktorów, ale jeśli nadal jest coś niejasnego, daj mi znać!

Levi Pearson
źródło
19
Cóż za znakomita odpowiedź! Należy wspomnieć, że Haskell często używa Maybetam, gdzie inne języki używają nulllub nil(z paskudnym NullPointerExceptionczającym się za każdym rogiem). Teraz inne języki również zaczynają używać tej konstrukcji: Scala as Option, a nawet Java 8 będzie miała ten Optionaltyp.
Landei
3
To doskonałe wyjaśnienie. Wiele wyjaśnień, które przeczytałem, wskazywało na ideę, że Just jest konstruktorem dla typu Może, ale żadne tak naprawdę nigdy tego nie wyrażało.
powtórz
@Landei, dzięki za sugestię. Dokonałem edycji, aby odnieść się do niebezpieczeństwa zerowych odniesień.
Levi Pearson
@Landei Typ opcji istnieje od ML w latach 70-tych, bardziej prawdopodobne jest, że Scala wybrała go stamtąd, ponieważ Scala używa konwencji nazewnictwa ML, Option z konstruktorami niektóre i żadne.
kamień kamieniarski
@Landei Apple Swift całkiem sporo wykorzystuje opcje
Jamin,
37

Większość obecnych odpowiedzi to wysoce techniczne wyjaśnienia, jak Justi przyjaciele pracują; Pomyślałem, że mógłbym spróbować swoich sił w wyjaśnieniu, do czego to służy.

Wiele języków ma taką wartość, nullktóra może być używana zamiast wartości rzeczywistej, przynajmniej w przypadku niektórych typów. To bardzo rozgniewało wiele osób i zostało powszechnie uznane za zły ruch. Mimo to czasami warto mieć wartość taką, jak nullwskazanie braku rzeczy.

Haskell rozwiązuje ten problem, zmuszając Cię do wyraźnego oznaczania miejsc, w których możesz mieć Nothing(jego wersję a null). Zasadniczo, jeśli twoja funkcja normalnie zwróci typ Foo, zamiast tego powinna zwrócić typ Maybe Foo. Jeśli chcesz wskazać, że nie ma wartości, zwróć Nothing. Jeśli chcesz zwrócić wartość bar, powinieneś zamiast tego zwrócić Just bar.

Więc w zasadzie, jeśli nie Nothingmożesz, nie potrzebujesz Just. Jeśli możesz Nothing, potrzebujesz Just.

Nie ma w tym nic magicznego Maybe; jest zbudowany na systemie typów Haskell. Oznacza to, że możesz użyć z nim wszystkich zwykłych sztuczek dopasowywania wzorców Haskella .

Brent Royal-Gordon
źródło
1
Bardzo ładna odpowiedź obok innych odpowiedzi, ale myślę, że
przydałby się
13

Dla danego typu twartość Just tjest istniejącą wartością typu t, gdzie Nothingreprezentuje nieosiągnięcie wartości lub przypadek, w którym posiadanie wartości byłoby bez znaczenia.

W Twoim przykładzie ujemne saldo nie ma sensu, więc jeśli coś takiego się wydarzy, zostanie zastąpione przez Nothing.

Na przykład można to wykorzystać do dzielenia, definiując funkcję dzielenia, która przyjmuje aand b, i zwraca, Just a/bjeśli bjest różna od zera, i w Nothingprzeciwnym razie. Jest często używany w ten sposób, jako wygodna alternatywa dla wyjątków lub jak w poprzednim przykładzie, do zastępowania wartości, które nie mają sensu.

qaphla
źródło
1
Powiedzmy, że w powyższym kodzie usunąłem Just, dlaczego to nie zadziała? Czy musisz mieć po prostu zawsze, gdy chcesz mieć nic? Co dzieje się z wyrażeniem, w którym funkcja wewnątrz tego wyrażenia nie zwraca nic?
powtórz
7
Gdybyś usunął Just, twój kod nie byłby typecheck. Powodem Justjest zachowanie właściwych typów. Istnieje typ (właściwie monada, ale łatwiej o nim myśleć jako o typie) Maybe t, który składa się z elementów formy Just ti Nothing. Ponieważ Nothingma typ Maybe t, wyrażenie, które może obliczyć jedną Nothinglub jakąś wartość typu, tnie jest poprawnie wpisane. Jeśli funkcja zwraca Nothingw niektórych przypadkach, każde wyrażenie używające tej funkcji musi mieć jakiś sposób na sprawdzenie tego ( isJustlub instrukcję case), aby obsłużyć wszystkie możliwe przypadki.
qaphla
2
Więc Just jest po prostu tam, aby być spójnym w typie Może, ponieważ zwykłe t nie jest w typie Może. Teraz wszystko jest znacznie jaśniejsze. Dziękuję Ci!
powtórz
3
@qaphla: Twój komentarz na temat tego, że jest to „monada, [...]” jest mylący. Maybe t to tylko typ. Fakt, że istnieje Monadinstancja dla Maybe, nie przekształca go w coś, co nie jest typem.
Sarah,
2

Całkowita funkcja a-> b może znaleźć wartość typu b dla każdej możliwej wartości typu a.

W Haskell nie wszystkie funkcje są totalne. W tym konkretnym przypadku funkcja lendnie jest całkowita - nie jest zdefiniowana dla przypadku, gdy saldo jest mniejsze niż rezerwa (chociaż według mojego gustu bardziej sensownym byłoby niedopuszczenie, aby newBalance było mniejsze niż rezerwa - tak jak jest, możesz pożyczyć 101 z saldo 100).

Inne projekty, które dotyczą funkcji niecałkowitych:

  • rzut wyjątków po sprawdzeniu wartości wejściowej nie mieści się w zakresie
  • zwraca specjalną wartość (typ pierwotny): ulubionym wyborem jest wartość ujemna dla funkcji całkowitoliczbowych, które mają zwracać liczby naturalne (na przykład String.indexOf - gdy nie znaleziono podłańcucha, zwracany indeks jest zwykle zaprojektowany jako ujemny)
  • zwraca specjalną wartość (wskaźnik): NULL lub coś takiego
  • po cichu wrócić, nic nie robiąc: na przykład lendmożna napisać, aby zwrócić stare saldo, jeśli warunek pożyczki nie zostanie spełniony
  • zwraca specjalną wartość: Nothing (lub Left zawijając jakiś obiekt opisu błędu)

Są to niezbędne ograniczenia projektowe w językach, które nie mogą egzekwować wszystkich funkcji (na przykład Agda może, ale prowadzi to do innych komplikacji, takich jak niekompletność turinga).

Problem z zwracaniem specjalnej wartości lub wyrzucaniem wyjątków polega na tym, że wywołujący łatwo omyłkowo pominąć obsługę takiej możliwości.

Problem z dyskretnym odrzucaniem awarii jest również oczywisty - ograniczasz to, co dzwoniący może zrobić z funkcją. Na przykład, jeśli lendzwróci stare saldo, dzwoniący nie ma możliwości dowiedzenia się, czy saldo się zmieniło. Może to stanowić problem, ale nie musi, w zależności od zamierzonego celu.

Rozwiązanie Haskella zmusza obiekt wywołujący funkcję częściową do radzenia sobie z typem takim jak Maybe alub z Either error apowodu zwracanego typu funkcji.

W ten sposób, lendzgodnie z definicją, jest to funkcja, która nie zawsze oblicza nowy bilans - w pewnych okolicznościach nowy bilans nie jest zdefiniowany. Sygnalizujemy tę okoliczność dzwoniącemu, zwracając specjalną wartość Nothing lub opakowując nową równowagę w Just. Dzwoniący ma teraz swobodę wyboru: albo poradzi sobie z niepowodzeniem pożyczki w specjalny sposób, albo zignoruje i skorzysta ze starego salda - na przykład maybe oldBalance id $ lend amount oldBalance.

Sassa NF
źródło
-1

Funkcja if (cond :: Bool) then (ifTrue :: a) else (ifFalse :: a)musi mieć ten sam typ ifTruei ifFalse.

Więc kiedy piszemy then Nothing, musimy użyć Maybe atypu inelse f

if balance < reserve
       then (Nothing :: Maybe nb)         -- same type
       else (Just newBalance :: Maybe nb) -- same type
dowcip
źródło
1
Jestem pewien, że chciałeś tu powiedzieć coś naprawdę głębokiego
patrz
1
Haskell ma wnioskowanie o typie. Nie ma potrzeby, aby podać typ Nothingi Just newBalancejawny.
prawostronny
W tym wyjaśnieniu dla niewtajemniczonych, wyraźne typy wyjaśniają znaczenie użycia Just.
philip,