Najczystszy sposób zgłaszania błędów w Haskell

22

Pracuję nad nauką języka Haskell i natknąłem się na trzy różne sposoby radzenia sobie z błędami w funkcjach, które piszę:

  1. Potrafię po prostu pisać error "Some error message.", co rzuca wyjątek.
  2. Mogę zwrócić funkcję Maybe SomeType, w której mogę, ale nie muszę, zwrócić tego, co chciałbym zwrócić.
  3. Mogę mieć funkcję return Either String SomeType, w której mogę zwrócić komunikat o błędzie lub to, o co zostałem poproszony.

Moje pytanie brzmi: Jakiej metody radzenia sobie z błędami powinienem używać i dlaczego? Może powinienem stosować różne metody, w zależności od kontekstu?

Moje obecne rozumienie to:

  • „Trudno” radzić sobie z wyjątkami w kodzie czysto funkcjonalnym, aw Haskell chce się zachować jak najbardziej funkcjonalną funkcjonalność.
  • Zwracanie Maybe SomeTypejest właściwą rzeczą, jeśli funkcja zawiedzie lub odniesie sukces (tj. Nie ma różnych sposobów, aby mogła zawieść ).
  • Zwracanie Either String SomeTypejest właściwą rzeczą, jeśli funkcja może zawieść na jeden z różnych sposobów.
CmdrMoozy
źródło

Odpowiedzi:

32

Dobra, pierwsza zasada obsługi błędów w Haskell: Nigdy nie używajerror .

To jest po prostu okropne pod każdym względem. Istnieje wyłącznie jako akt historii, a fakt, że używa go Preludium, jest okropny. Nie używaj tego.

Jedyny możliwy czas, w którym można go użyć, to gdy coś jest tak wewnętrznie okropne, że coś musi być nie tak z samą strukturą rzeczywistości, co powoduje, że wyniki programu są dyskusyjne.

Teraz pojawia się pytanie Maybevs Either. Maybejest dobrze dostosowany do czegoś takiego head, co może, ale nie musi zwrócić wartość, ale istnieje tylko jeden możliwy powód niepowodzenia. Nothingmówi coś takiego: „zepsuło się, a ty już wiesz dlaczego”. Niektórzy twierdzą, że oznacza to funkcję częściową.

Najbardziej niezawodną formą obsługi błędów jest Either+ ADT błąd.

Na przykład w jednym z moich kompilatorów hobby mam coś takiego

data CompilerError = ParserError ParserError
                   | TCError TCError
                   ...
                   | ImpossibleError String

data ParserError = ParserError (Int, Int) String
data TCError = CouldntUnify Ty Ty
             | MissingDefinition Name
             | InfiniteType Ty
             ...

type ErrorM m = ExceptT CompilerError m -- from MTL

Teraz definiuję kilka rodzajów błędów, zagnieżdżając je tak, że mam jeden wspaniały błąd najwyższego poziomu. Może to być błąd na dowolnym etapie kompilacji lub błąd ImpossibleError, który oznacza błąd kompilatora.

Każdy z tych typów błędów stara się przechowywać jak najwięcej informacji tak długo, jak to możliwe, aby uzyskać ładny wydruk lub inną analizę. Co ważniejsze, nie mając łańcucha, mogę przetestować, że uruchomienie niepoprawnego programu przez moduł sprawdzania typów faktycznie generuje błąd unifikacji! Kiedy coś jest String, zniknie na zawsze, a wszelkie zawarte w nim informacje są nieprzejrzyste dla kompilatora / testów, więc też Either Stringnie jest świetne.

Na koniec pakuję ten typ w ExceptTnowy transformator monadowy od MTL. Jest to zasadniczo EitherTi zawiera niezłą porcję funkcji do rzucania i wyłapywania błędów w czysty, przyjemny sposób.

Na koniec warto wspomnieć, że Haskell ma mechanizmy obsługi wyjątków, tak jak inne języki, z wyjątkiem tego, że łapanie wyjątku trwa IO. Wiem, że niektórzy ludzie lubią używać ich do IOciężkich aplikacji, w których wszystko może potencjalnie zawieść, ale tak rzadko, że nie lubią o tym myśleć. Niezależnie od tego, czy korzystasz z tych nieczystych wyjątków, czy po prostu ExceptT Error IOjest to kwestia gustu. Osobiście wybieram, ExceptTponieważ lubię przypominać sobie o szansie niepowodzenia.


Podsumowując

  • Maybe - Mogę zawieść w jeden oczywisty sposób
  • Either CustomType - Mogę zawieść, a powiem ci, co się stało
  • IO+ wyjątki - czasami mi się nie udaje. Sprawdź moje dokumenty, aby zobaczyć, co rzucam, kiedy to robię
  • error - Ja też cię nienawidzę
Daniel Gratzer
źródło
Ta odpowiedź bardzo mnie wyjaśnia. Zastanawiałem się nad tym, że wbudowane funkcje lubią headlub lastwydają się używać error, więc zastanawiałem się, czy to był dobry sposób na zrobienie czegoś i po prostu czegoś mi brakowało. To odpowiada na to pytanie. :)
CmdrMoozy,
5
@CmdrMoozy Cieszę się, że mogę pomóc :) To naprawdę niefortunne, że preludium ma tak złe praktyki. Taki jest sposób na dziedzictwo: /
Daniel Gratzer
3
+1 Twoje podsumowanie jest niesamowite
recursion.ninja
2

Nie rozdzieliłbym 2 i 3 na podstawie tego, na ile sposobów coś może zawieść. Gdy tylko pomyślisz, że jest tylko jeden możliwy sposób, inny pokaże swoją twarz. Metryka powinna brzmieć: „czy moim rozmówcom zależy na tym, dlaczego coś się nie udało?

Poza tym nie jest dla mnie od razu jasne, że Either String SomeTypemoże powodować wystąpienie błędu. Zrobiłbym prosty algebraiczny typ danych z warunkami o bardziej opisowej nazwie.

To, którego używasz, zależy od charakteru napotkanego problemu i idiomów pakietu oprogramowania, z którym pracujesz. Chociaż starałbym się unikać # 1.

Telastyn
źródło
W rzeczywistości używanie Eitherbłędów jest bardzo dobrze znanym wzorcem w Haskell.
Rufflewind,
1
@rufflewind - Eithernie jest niejasną częścią, użycie Stringjako błędu, a nie rzeczywistego ciągu.
Telastyn
Ach, źle odczytałem twój zamiar.
Rufflewind,