2015-08-08 9 views
8

Aktualnie uczę się Haskella. Jedną z motywacji, dla których wybrałem ten język, było napisanie oprogramowania o bardzo wysokim stopniu odporności, tj. Funkcji, które są w pełni zdefiniowane, matematycznie deterministyczne i nigdy nie powodują awarii ani nie powodują błędów. Nie chodzi mi o awarię spowodowaną przez predykaty systemu ("system z pamięci", "komputer w ogniu" itp.), Nie są one interesujące i mogą po prostu załamać cały proces. Nie mam również na myśli błędnego zachowania spowodowanego przez nieważne deklaracje (pi = 4).Solidny haskell bez błędów

Zamiast tego odnoszę się do błędów spowodowanych przez błędne stany, które chcę usunąć, czyniąc te stany niereprezentowalnymi i nie nadającymi się do skompilowania (w niektórych funkcjach) poprzez ścisłe pisanie statyczne. W moim umyśle nazwałem te funkcje "czystymi" i uznałem, że silny system typów pozwoli mi to osiągnąć. Jednak Haskell nie definiuje "czystego" w ten sposób i pozwala programom na awarię przez error w dowolnym kontekście.

Why is catching an exception non-pure, but throwing an exception is pure?

Jest to całkowicie dopuszczalne i nie jest dziwne w ogóle. Niezadowalające jest jednak to, że Haskell nie wydaje się oferować pewnej funkcjonalności, aby zabronić definicji funkcji, które mogłyby prowadzić do oddziałów przy użyciu error.

Oto zmyślony przykład dlaczego uważam to rozczarowujące:

module Main where 
import Data.Maybe 

data Fruit = Apple | Banana | Orange Int | Peach 
    deriving(Show) 

readFruit :: String -> Maybe Fruit 
readFruit x = 
    case x of 
     "apple" -> Just Apple 
     "banana" -> Just Banana 
     "orange" -> Just (Orange 4) 
     "peach" -> Just Peach 
     _ -> Nothing 

showFruit :: Fruit -> String 
showFruit Apple = "An apple" 
showFruit Banana = "A Banana" 
showFruit (Orange x) = show x ++ " oranges" 

printFruit :: Maybe Fruit -> String 
printFruit x = showFruit $ fromJust x 

main :: IO() 
main = do 
    line <- getLine 
    let fruit = readFruit line 
    putStrLn $ printFruit fruit 
    main 

Powiedzmy, że jestem paranoikiem, że czysta funkcje readFruit i printFruit naprawdę nie powiedzie się z powodu stanów niedotykane. Można sobie wyobrazić, że kodeks ma na celu wystrzelenie rakiety pełnej astronautów, która w absolutnie krytycznej rutynie potrzebuje serializacji i odseparowania wartości owoców.

Pierwszym niebezpieczeństwem jest naturalne, że popełniliśmy błąd w dopasowywaniu wzorców, ponieważ daje to nam przerażające błędne stany, których nie można obsłużyć. Na szczęście Haskell dostarcza zbudowany w taki sposób, aby zapobiec tych, po prostu skompilować nasz program z -Wall który obejmuje -fwarn-incomplete-patterns i AHA:

src/Main.hs:17:1: Warning: 
    Pattern match(es) are non-exhaustive 
    In an equation for ‘showFruit’: Patterns not matched: Peach 

Zapomnieliśmy serializacji brzoskwini i showFruit byłby wyrzucony błąd. To się łatwo naprawić, po prostu dodajemy:

showFruit Peach = "A peach" 

Program kompiluje się bez ostrzeżenia, niebezpieczeństwo zażegnane! Uruchamiamy rakietę ale nagle program wywala się z:

Maybe.fromJust: Nothing 

Rakieta jest skazany i uderza ocean spowodowane następującymi wadliwego line:

printFruit x = showFruit $ fromJust x 

Zasadniczo fromJust ma oddział, gdzie podnosi Error więc nie chcieliśmy, aby program się kompilował, jeśli spróbujemy go użyć od printFruit absolutnie musi być "super" czysty. Mogliśmy ustalona, ​​że ​​na przykład poprzez zastąpienie linii z:

printFruit x = maybe "Unknown fruit!" (\y -> showFruit y) x 

Uważam to za dziwne, że Haskell zdecyduje się wprowadzić ścisłe wpisywanie i niekompletny wykrywanie wzorców, wszystko w pogoni nobel zapobiegania nieprawidłowe stany od przedstawianego ale upadki tuż przed linią mety, nie dając programistom sposobu na wykrycie gałęzi na error, gdy nie są dozwolone.W pewnym sensie sprawia to, że Haskell jest mniej odporny niż Java, która zmusza Cię do zadeklarowania wyjątków, które twoje funkcje mogą podnieść.

Najbardziej banalnym sposobem wdrożenia tego byłoby po prostu nieokreślone error w jakiś sposób, lokalnie dla funkcji i dowolnej funkcji używanej przez jej równanie przez jakąś formę powiązanej deklaracji. Jednak nie wydaje się to możliwe.

The wiki page about errors vs exceptions wymienia w tym celu rozszerzenie o nazwie "Extended Static Checking", ale prowadzi to po prostu do zerwania łącza.

W zasadzie sprowadza się do: Jak uzyskać powyższy program, aby nie skompilować, ponieważ używa on fromJust? Wszystkie pomysły, sugestie i rozwiązania są mile widziane.

+1

(1) Uważam, że [są to artykuły, których szukasz] (http://research.microsoft.com/~simonpj/papers/verify/index.htm). (2) Zwróć uwagę, że 'printFruit' jest niepotrzebny. Jeśli użytkownik chce wyświetlić 'Maybe Fruit' może użyć' fmap showFruit', a następnie rozwinąć 'Maybe' w rozsądny sposób. To oczywiście nie odpowiada na twoje pytanie (dobrze byłoby trzymać użytkowników z dala od 'fromJust' również!), Więc kontynuuj :) – duplode

+3

Chciałbym, aby Haskell miał coś w rodzaju sprawdzania kompletności. Byłbym nawet usatysfakcjonowany sprawdzaniem bez wyjątku, sprawdzającym, czy nie ma kompletności i wykorzystania funkcji częściowych, ale nie sprawdzając nieskończonych pętli. Chciałbym również wprowadzić takie ostrzeżenia w błąd, tak że nie mogę ich łatwo przeoczyć. To prawdopodobnie nie wymagałoby dużego nakładu pracy w GHC. – chi

+2

Czy spojrzałeś na Idris? To może być język, którego pragniesz. – user3237465

Odpowiedz

1

Odpowiedź była taka, że ​​chciałem mieć poziom sprawdzania całości, którego Haskell niestety nie zapewnia (jeszcze?).

Alternatywnie chcę mieć typy zależne (na przykład Idris) lub statyczny weryfikator (np. Liquid Haskell) lub wykrywanie lint składni (np. hlint).

Naprawdę zajmuję się teraz Idrisem, wydaje się niesamowitym językiem. Oto talk by the founder of Idris, które mogę polecić oglądać.

Kredyty za te odpowiedzi otrzymują: @duplode, @chi, @ user3237465 i @luqui.

1

Twoja główna skarga dotyczy numeru fromJust, ale biorąc pod uwagę, że jest to zasadniczo sprzeczne z Twoimi celami, dlaczego go używasz?

Pamiętaj, że najważniejszym powodem dla error jest to, że nie wszystko można zdefiniować w sposób gwarantujący system typów, więc posiadanie "To nie może się zdarzyć, zaufaj mi" ma sens. fromJust pochodzi z tego sposobu myślenia "czasami wiem lepiej niż kompilator". Jeśli nie zgadzasz się z tym nastawieniem, nie używaj go. Ewentualnie nie nadużywaj tego tak, jak robi to przykład.

EDIT:

Aby rozwinąć na moim punkcie, myśleć o tym, jak można zaimplementować co prosicie (ostrzeżenia dotyczące korzystania z funkcji częściowe). Gdzie pojawi się ostrzeżenie w poniższym kodzie?

a = fromJust $ Just 1 
b = Just 2 
c = fromJust $ c 
d = fromJust 
e = d $ Just 3 
f = d b 

Wszystkie te elementy nie są statyczne. (Przepraszam, jeśli składnia następny jest wyłączony, jest późno)

g x = fromJust x + 1 
h x = g x + 1 
i = h $ Just 3 
j = h $ Nothing 
k x y = if x > 0 then fromJust y else x 
l = k 1 $ Just 2 
m = k (-1) $ Just 3 
n = k (-1) $ Nothing 

i tutaj jest znowu bezpieczne, ale j nie jest. Która metoda powinna zwrócić ostrzeżenie? Co robimy, gdy niektóre lub wszystkie z tych metod są zdefiniowane poza naszą funkcją. Co robisz w przypadku k, gdzie jest warunkowo częściowy? Czy niektóre lub wszystkie te funkcje zawiodą?

Wykonując połączenie, kompilator łączy wiele skomplikowanych problemów. Zwłaszcza jeśli weźmiesz pod uwagę, że ostrożny wybór bibliotek go unika.

W obu przypadkach rozwiązaniem IMO dla tego rodzaju problemów jest użycie narzędzia po lub w trakcie kompilacji w celu poszukiwania problemów, zamiast modyfikowania kompilatora. Takie narzędzie może łatwiej zrozumieć "twój kod" w porównaniu do "jego kodu" i lepiej określić, gdzie najlepiej ostrzec w przypadku niewłaściwego użycia. Można również dostroić go bez konieczności dodawania wiązki adnotacji do pliku źródłowego, aby uniknąć fałszywych alarmów. Nie znam jakiegoś narzędzia z offhandu, a proponuję jedno.

+2

Podchodzisz do mojego pytania pod niewłaściwym kątem. Poniższy przykład ilustruje, w jaki sposób programista może omyłkowo wprowadzić ścieżkę błędu. Błąd może być również bardziej subtelny, jak zezwolenie na odgałęzienie, w którym możliwy jest podział przez zero. Założenie "dobrze, tylko nie róbmy błędów" sprawia, że ​​sam system typu jest w dużej mierze bezcelowy. Przyznajesz też, że funkcja jest bardziej niebezpieczna, czy nie byłoby użyteczne, gdyby kompilatorowi udało się zapobiec wywołaniom takich niebezpiecznych funkcji? –

+0

@HannesLandeholm: Ja poszerzę swoją odpowiedź, ale nie jest to przypadek "nie rób błędów", "nie używaj narzędzi, które nie zgadzają się z twoim paradygmatem". Jeśli 'fromJust' i jej ilk mogą złamać guallees, to twoja argumentacja byłaby warta, ale' _ | _' jest zawsze czymś, co może się wkraść. – Guvante

+2

Niedawno zacząłem pracować nad 7-milimetrową bazą kodu (nie w Haskell), więc założyłem ten obiektyw, czytając pytanie OP. "Nie używaj X" nie jest tak naprawdę dopuszczalnym rozwiązaniem w tak ogromnym miejscu. Konieczne byłoby sprawdzenie narzędzia w takich przypadkach. FWIW ['hlint'] (http://community.haskell.org/~ndm/hlint/) zrobi to, jeśli dobrze pamiętam. – luqui

4

Haskell pozwala na dowolną rekurencję ogólną, więc nie jest tak, że wszystkie programy Haskell byłyby z konieczności całkowite, gdyby tylko error w różnych formach zostały usunięte. Oznacza to, że możesz zdefiniować error a = error a, aby zająć się każdym przypadkiem, którego nie chcesz obsługiwać. Błąd runtime jest po prostu bardziej przydatny niż nieskończona pętla.

Nie należy myśleć o error jako podobnej do wyjątku Java w ogrodzie. error to błąd asercji reprezentujący błąd programowania, a funkcja taka jak fromJust, która może wywoływać error jest asercją.Nie powinieneś próbować wychwycić wyjątku wyprodukowanego przez error, z wyjątkiem specjalnych przypadków, takich jak serwer, który musi nadal działać, nawet jeśli program obsługi żąda zgłoszenia błędu programistycznego.

+0

Nie powiedziałbym, że termin "asercja" jest w tym przypadku oczywisty, ponieważ zwykle jest związany z konstrukcją dokumentacji/debugowania, która może być bezpiecznie usunięta ze względu na wydajność w innych językach i nie ma wpływu na samą logikę funkcjonalną programu . Przeciwnie, błąd w Haskell jest bardziej podobny do błędu dynamicznego rzucania w innych językach. Kiedy dowolne funkcje mogą je rzucić, idziesz po polu minowym, jeśli piszesz solidne oprogramowanie i nawet kompilator nie może ci pomóc. Jedno błędne połączenie API i jesteś skazany na zagładę. –

+2

@hanneslandeholm, powinieneś rozważyć języki takie jak Coq i Agda. Haskell nie jest stworzony do tego, co chcesz, chociaż oferuje dobre narzędzia do pisania bezpiecznego i poprawnego kodu. Do tej pory nikt nie potrafił napisać w sposób niezawodny, silnie normalizujący, który jest odpowiedni także do praktycznego programowania. Haskell to kompromis. – dfeuer

+1

@dfeuer, dlaczego Idris nie nadaje się do praktycznego programowania? Ma odpowiedni system efektów, kompiluje się do C, wykonuje różne optymalizacje, ma część syntaktycznego cukru, odpowiednie dziury typu (dobrze, Agdy są lepsze), taktyki i inne praktyczne rzeczy.Porównaj to z super brzydkimi transformatorami Monada i instytutem [hasochizm] (https://personal.cis.strath.ac.uk/conor.mcbride/pub/hasochism.pdf). Praktycznie już napisany język programowania już istnieje, ma po prostu czysty ekosystem. – user3237465

Powiązane problemy