Co dokładnie sprawia, że ​​system typu Haskell jest tak czczony (w porównaniu z Javą)?


204

Zaczynam się uczyć Haskella . Jestem bardzo nowy i czytam tylko kilka książek online, aby omówić jego podstawowe konstrukcje.

Jednym z „memów”, o których często go znają ludzie, jest cała rzecz „jeśli się skompiluje, zadziała *” - co moim zdaniem jest związane z siłą systemu typów.

Próbuję zrozumieć, dlaczego właśnie Haskell jest lepszy niż inne statycznie pisane języki pod tym względem.

Innymi słowy, zakładam, że w Javie można zrobić coś haniebnego, na przykład zakopać, ArrayList<String>()aby zawierać coś, co naprawdę powinno być ArrayList<Animal>(). Obrzydliwe jest to, że stringzawiera elephant, giraffe, itp., A jeśli ktoś w to Mercedeswłączy - kompilator ci nie pomoże.

Jeśli tak zrobię ArrayList<Animal>(), to w późniejszym czasie, jeśli zdecyduję, że mój program nie jest tak naprawdę o zwierzętach, dotyczy pojazdów, wtedy mogę zmienić, powiedzmy, funkcję, która produkuje, ArrayList<Animal>aby produkować, ArrayList<Vehicle>a moje IDE powinno mi powiedzieć wszędzie to przerwa w kompilacji.

Zakładam, że to właśnie ludzie rozumieją przez silny system typów, ale nie jest dla mnie oczywiste, dlaczego Haskell jest lepszy. Innymi słowy, możesz napisać dobrą lub złą Javę, zakładam, że możesz zrobić to samo w Haskell (tzn. Wstawiać rzeczy w ciągi znaków / ints, które powinny być naprawdę pierwszorzędnymi typami danych).

Podejrzewam, że brakuje mi czegoś ważnego / podstawowego.
Byłbym bardzo szczęśliwy, gdybym pokazał błąd moich działań!


31
Pozwolę ludziom więcej wiedzy niż piszę prawdziwe odpowiedzi, ale sedno tego jest następujące: języki o statycznym typie tekstu, takie jak C #, mają system typów, który próbuje pomóc ci napisać dający się obronić kod; wpisz systemy takie jak próba Haskella, aby pomóc Ci napisać poprawny (tzn. możliwy do udowodnienia) kod. Podstawową zasadą w pracy jest przenoszenie rzeczy, które można sprawdzić na etapie kompilacji; Haskell sprawdza więcej rzeczy podczas kompilacji.
Robert Harvey

8
Niewiele wiem o Haskell, ale mogę mówić o Javie. Chociaż wydaje się mocno napisany na maszynie, wciąż pozwala ci robić „haniebne” rzeczy, jak powiedziałeś. W przypadku prawie każdej gwarancji Java dotyczącej systemu typów istnieje na to sposób.

12
Nie wiem, dlaczego wszystkie odpowiedzi wspominają Maybetylko pod koniec. Gdybym musiał wybrać tylko jedną rzecz, którą bardziej popularne języki powinny pożyczyć od Haskell, to byłoby to. To bardzo prosty pomysł (z teoretycznego punktu widzenia niezbyt interesujący), ale już samo to znacznie ułatwiłoby nam pracę.
Paul

1
Znajdziesz tu świetne odpowiedzi, ale próbując pomóc, przestudiuj podpisy typów. Pozwalają ludziom i programom na rozumowanie programów w sposób, który zilustruje sposób, w jaki Java jest w mętnych środkowych typach re:.
Michael Wielkanoc

6
Dla uczciwości muszę zaznaczyć, że „całość, jeśli się skompiluje, zadziała” to hasło, a nie dosłowne stwierdzenie faktu. Tak, my programiści Haskell wiemy, że przekazanie sprawdzania typu daje dużą szansę na poprawność, w przypadku niektórych ograniczonych pojęć poprawności, ale z pewnością nie jest to dosłownie i uniwersalnie „prawdziwe” stwierdzenie!
Tom Ellis,

Odpowiedzi:


230

Oto nieuporządkowana lista funkcji systemu typów dostępnych w Haskell i niedostępnych lub mniej przyjemnych w Javie (według mojej wiedzy, która jest wprawdzie słaba w Javie)

  • Bezpieczeństwo . Typy Haskella mają całkiem dobre właściwości „bezpieczeństwa typu”. Jest to dość specyficzne, ale w gruncie rzeczy oznacza, że ​​wartości określonego typu nie mogą dobrowolnie przekształcić się w inny typ. Czasami jest to sprzeczne ze zmiennością (patrz ograniczenie wartości OCamla )
  • Algebraiczne typy danych . Typy w Haskell mają zasadniczo taką samą strukturę jak matematyka w szkole średniej. Jest to oburzająco proste i konsekwentne, a jednak, jak się okazuje, tak potężne, jak tylko możesz. To po prostu świetna podstawa dla systemu typów.
    • Programowanie typu danych . To nie to samo, co typy ogólne (patrz generalizacja ). Zamiast tego, ze względu na prostotę struktury typu, jak wspomniano wcześniej, stosunkowo łatwo jest napisać kod, który działa ogólnie na tej strukturze. Później mówię o tym, jak Eqkompilator Haskell może automatycznie wyprowadzić coś takiego dla typu zdefiniowanego przez użytkownika. Zasadniczo sposób, w jaki to robi, polega na przejściu przez zwykłą, prostą strukturę leżącą u podstaw dowolnego typu zdefiniowanego przez użytkownika i dopasowaniu jej do wartości - bardzo naturalnej formie strukturalnej równości.
  • Typy wzajemnie rekurencyjne . Jest to tylko niezbędny element pisania nietrywialnych typów.
    • Typy zagnieżdżone . Pozwala to zdefiniować typy rekurencyjne względem zmiennych, które powtarzają się w różnych typach. Na przykład jednym typem zrównoważonych drzew jest data Bt a = Here a | There (Bt (a, a)). Pomyśl dokładnie o prawidłowych wartościach Bt ai zwróć uwagę, jak działa ten typ. To trudne!
  • Uogólnienie . Jest to prawie zbyt głupie, aby nie mieć w systemie pisma (hmm, patrzę na ciebie, idź). Ważne jest, aby mieć pojęcie o zmiennych typu i umiejętność mówienia o kodzie, który jest niezależny od wyboru tej zmiennej. Hindley Milner jest systemem typów wywodzącym się z Systemu F. System typów Haskella jest rozwinięciem typowania HM, a System F jest zasadniczo trzonem uogólnienia. Mam na myśli to, że Haskell ma bardzo dobrą historię uogólnienia.
  • Typy abstrakcyjne . Historia Haskella tutaj nie jest świetna, ale też nie istnieje. Możliwe jest pisanie typów, które mają publiczny interfejs, ale prywatną implementację. To pozwala nam zarówno zaakceptować zmiany w kodzie implementacyjnym w późniejszym czasie, a co ważne, ponieważ jest to podstawą wszystkich operacji w Haskell, piszemy typy „magiczne”, które mają dobrze zdefiniowane interfejsy, takie jak IO. Szczerze mówiąc, Java prawdopodobnie ma ładniejszą historię typu abstrakcyjnego, ale nie sądzę, że dopóki interfejsy nie stały się bardziej popularne, to była prawdziwa prawda.
  • Parametryczność . Wartości Haskell nie mają żadnych operacji uniwersalnych. Java narusza to poprzez takie rzeczy, jak równość referencyjna i mieszanie, a jeszcze bardziej rażąco poprzez przymus. Oznacza to, że otrzymujesz bezpłatne twierdzenia o typach, które pozwalają poznać znaczenie operacji lub wartość w znaczącym stopniu całkowicie z jej typu - niektóre typy są takie, że może być tylko niewielka liczba mieszkańców.
  • Typy o wyższym rodzaju wyświetlają wszystkie typy podczas kodowania trudniejszych rzeczy. Functor / Applicative / Monad, Foldable / Traversable, cały mtlsystem typowania efektów, uogólnione punkty kontrolne funktora. Lista jest długa. Istnieje wiele rzeczy, które najlepiej wyrażać przy wyższych rodzajach, a stosunkowo niewiele systemów typu pozwala nawet użytkownikowi mówić o tych rzeczach.
  • Klasy typów . Jeśli myślisz o systemach typu jako o logice - co jest przydatne - wtedy często wymaga się od ciebie udowodnienia. W wielu przypadkach jest to zasadniczo szum linii: może istnieć tylko jedna właściwa odpowiedź i marnowanie czasu i wysiłku dla programisty na stwierdzenie tego. Klasy typów są dla Haskell sposobem na wygenerowanie dla Ciebie dowodów. Mówiąc bardziej konkretnie, pozwala to rozwiązać proste „układy równań typów”, takie jak „W jakim typie zamierzamy (+)razem działać? Och Integer, ok! Teraz wstawmy odpowiedni kod!”. W bardziej złożonych systemach możesz tworzyć bardziej interesujące ograniczenia.
    • Rachunek więzów . Ograniczenia w Haskell - które są mechanizmem sięgania do systemu prologów klasowych - są typowane strukturalnie. Daje to bardzo prostą formę relacji podsieci, która pozwala łączyć złożone ograniczenia z prostszych. Cała mtlbiblioteka oparta jest na tym pomyśle.
    • Wynikających . Aby sterować kanonicznością systemu klas, konieczne jest napisanie wielu często trywialnych kodów opisujących ograniczenia, jakie muszą tworzyć instancje zdefiniowane przez użytkownika. Postępuj zgodnie z bardzo normalną strukturą typów Haskell, często można poprosić kompilator o wykonanie tej płyty dla Ciebie.
    • Prolog klasy typu . Solver klasy typu Haskell - system generujący te „dowody”, o których wspominałem wcześniej - jest zasadniczo kaleką formą Prologa o ładniejszych właściwościach semantycznych. Oznacza to, że możesz zakodować naprawdę włochate rzeczy w prologu typu i oczekiwać, że będą obsługiwane przez cały czas kompilacji. Dobrym przykładem może być rozwiązanie dowodu, że dwie listy heterogeniczne są równoważne, jeśli zapomnisz o kolejności - są to równoważne heterogeniczne „zestawy”.
    • Klasy typów wieloparametrowych i zależności funkcjonalne . Są to po prostu bardzo przydatne udoskonalenia bazowego prologu typu maszynowego. Jeśli znasz Prolog, możesz wyobrazić sobie, jak bardzo wzrośnie moc ekspresji, kiedy możesz napisać predykaty więcej niż jednej zmiennej.
  • Całkiem dobre wnioskowanie . Języki oparte na systemach typu Hindley Milner mają całkiem dobre wnioski. Sam HM ma pełne wnioskowanie, co oznacza, że ​​nigdy nie musisz pisać zmiennej typu. Haskell 98, najprostsza forma Haskell, rzuca to już w bardzo rzadkich okolicznościach. Ogólnie rzecz biorąc, współczesny Haskell był eksperymentem polegającym na powolnym zmniejszaniu przestrzeni pełnego wnioskowania przy jednoczesnym dodawaniu większej mocy do HM i sprawdzaniu, kiedy użytkownicy narzekają. Ludzie bardzo rzadko narzekają - wniosek Haskella jest całkiem dobry.
  • Tylko bardzo, bardzo, bardzo słabe podtypowanie . Wspomniałem wcześniej, że system ograniczeń z prologu typu maszynowego ma pojęcie podtypu strukturalnego. To jedyna forma podtypu w Haskell . Składanie ofert jest okropne z punktu widzenia rozumowania i wnioskowania. Sprawia, że ​​każdy z tych problemów jest znacznie trudniejszy (system nierówności zamiast systemu równości). Bardzo łatwo jest też źle zrozumieć (Czy podklasa jest tym samym, co subtyping? Oczywiście, że nie! Ale ludzie bardzo często mylą to, a wiele języków pomaga w tym zamieszaniu! Jak się tu znaleźliśmy? Przypuszczam, że nikt nigdy nie bada LSP.)
    • Zauważ niedawno (na początku 2017 r.) Steven Dolan opublikował swoją pracę magisterską na temat MLsub , wariantu wnioskowania typu ML i Hindley-Milnera, który ma bardzo ładną historię subtypingu ( patrz także ). To nie umniejsza tego, co napisałem powyżej - większość systemów subtyfikacji jest zepsuta i ma złe wnioski - ale sugeruje, że właśnie dzisiaj mogliśmy odkryć obiecujące sposoby na pełne wnioskowanie i ładną zabawę. Teraz, aby być całkowicie jasnym, pojęcia podsieci Javy w żaden sposób nie mogą wykorzystywać algorytmów i systemów Dolana. Wymaga to przemyślenia, co oznacza podtypowanie.
  • Wyższe typy rang . Mówiłem o uogólnieniu wcześniej, ale więcej niż zwykłe uogólnienie warto rozmawiać o typach, które zawierają w sobie zmienne uogólnione . Na przykład odwzorowanie między strukturami wyższego rzędu, które jest nieświadome (patrz parametryczność ), na to, co te struktury „zawierają” ma podobny typ (forall a. f a -> g a). W prostej HM można napisać funkcję w tym rodzaju, ale z typami wyższej rangi żądacie takiej funkcji jako argumentu tak: mapFree :: (forall a . f a -> g a) -> Free f -> Free g. Zauważ, że azmienna jest związana tylko w argumencie. Oznacza to, że osoba definiująca funkcję mapFreedecyduje o tym, aw której chwili jest tworzona instancja, a nie użytkownik mapFree.
  • Rodzaje egzystencjalne . Podczas gdy typy wyższej rangi pozwalają nam mówić o kwantyfikator ogólny, typy egzystencjalne mówmy o kwantyfikator egzystencjalny: pomysł, że nie tylko istnieje jakiś nieznany rodzaj zaspokojenia pewnych równań. To przydaje się i dłużej trwało to długo.
  • Wpisz rodziny . Czasami mechanizmy klasowe są niewygodne, ponieważ nie zawsze myślimy w Prologu. Rodziny typów pozwalają nam pisać proste relacje funkcjonalne między typami.
    • Rodziny typu zamkniętego . Rodziny typów są domyślnie otwarte, co jest irytujące, ponieważ oznacza to, że chociaż można je rozszerzyć w dowolnym momencie, nie można ich „odwrócić” bez żadnej nadziei na sukces. Wynika to z faktu, że nie można udowodnić iniekcji , ale z rodzinami typu zamkniętego można to zrobić.
  • Rodzaje indeksowanych typów i promocja typów . W tym momencie robię się naprawdę egzotyczny, ale od czasu do czasu mają one praktyczne zastosowanie. Jeśli chcesz napisać rodzaj uchwytów, które są otwarte lub zamknięte, możesz to zrobić bardzo ładnie. Zwróć uwagę w poniższym fragmencie, który Statejest bardzo prostym typem algebraicznym, którego wartości również zostały wypromowane na poziomie typu. Następnie możemy mówić o konstruktorach typów, takich Handlejak przyjmowanie argumentów dla określonych rodzajów, takich jak State. Rozumienie wszystkich szczegółów jest mylące, ale także bardzo poprawne.

    data State = Open | Closed
    
    data Handle :: State -> * -> * where
      OpenHandle :: {- something -} -> Handle Open a
      ClosedHandle :: {- something -} -> Handle Closed a
  • Reprezentacje typu środowiska wykonawczego, które działają . Java jest znana z tego, że wymazuje czcionkę i ma tę cechę deszczu podczas parad niektórych ludzi. Usuwanie typu jest jednak właściwą drogą, ponieważ jeśli masz funkcję, getRepr :: a -> TypeReprto co najmniej naruszasz parametryczność. Co gorsza, jeśli jest to funkcja generowana przez użytkownika, która służy do wyzwalania niebezpiecznych koercji w środowisku wykonawczym ... masz poważne obawy dotyczące bezpieczeństwa . TypeableSystem Haskell pozwala stworzyć sejf coerce :: (Typeable a, Typeable b) => a -> Maybe b. Ten system opiera się na Typeableimplementacji w kompilatorze (a nie na przestrzeni użytkownika), a także nie mógłby otrzymać tak ładnej semantyki bez mechanizmu typklasy Haskell i praw, których przestrzeganie jest gwarantowane.

Jednak nie tylko te wartości systemu typowego Haskell odnoszą się również do tego, jak typy opisują język. Oto kilka cech Haskell, które napędzają wartość poprzez system typów.

  • Czystość . Haskell nie dopuszcza żadnych skutków ubocznych dla bardzo, bardzo, bardzo szerokiej definicji „efektu ubocznego”. Zmusza to do umieszczenia większej ilości informacji w typach, ponieważ typy rządzą danymi wejściowymi i wyjściowymi, a bez efektów ubocznych wszystko należy uwzględnić w danych wejściowych i wyjściowych.
    • IO . Następnie Haskell potrzebował sposobu, aby porozmawiać o skutkach ubocznych - ponieważ każdy prawdziwy program musi zawierać niektóre - więc kombinacja klas, typów o wyższym typie i typów abstrakcyjnych zrodziła pomysł użycia określonego, super-specjalnego typu, IO aktóry jest reprezentowany obliczenia uboczne, których wynikiem są wartości typu a. To podstawa bardzo ładnego systemu efektów osadzonego w czystym języku.
  • Braknull . Wszyscy wiedzą, że nulljest to miliard dolarów błąd współczesnych języków programowania. Typy algebraiczne, w szczególności możliwość dołączenia stanu „nie istnieje” do typów, które masz, poprzez przekształcenie typu Aw typ Maybe A, całkowicie łagodzą problem null.
  • Rekurencja polimorficzna . Pozwala to zdefiniować funkcje rekurencyjne, które uogólniają zmienne typu, mimo że używają ich w różnych typach w każdym wywołaniu rekurencyjnym we własnym uogólnieniu. Trudno o tym mówić, ale jest szczególnie przydatny w przypadku typów zagnieżdżonych. Spójrz z powrotem do Bt atypu sprzed i spróbuj napisać funkcję do obliczenia jego wielkość: size :: Bt a -> Int. Będzie to wyglądać trochę jak size (Here a) = 1i size (There bt) = 2 * size bt. Operacyjnie nie jest to zbyt skomplikowane, ale zauważ, że rekurencyjne wywołanie sizew ostatnim równaniu występuje w innym typie , ale ogólna definicja ma ładny ogólny typ size :: Bt a -> Int. Pamiętaj, że jest to funkcja, która łamie całkowite wnioskowanie, ale jeśli podasz podpis typu, Haskell pozwoli na to.

Mógłbym iść dalej, ale ta lista powinna sprawić, że zaczniesz - a potem - trochę.


7
Null nie był „błędem” o wartości miliarda dolarów. Zdarzają się przypadki, w których nie można statycznie zweryfikować, czy wskaźnik nie zostanie usunięty z dereferencji, zanim możliwe będzie istnienie jakiegokolwiek znaczącego ; próba pułapki dereferencyjnej w takim przypadku jest często lepsza niż wymaganie, aby wskaźnik początkowo zidentyfikował pozbawiony znaczenia obiekt. Myślę, że największym błędem związane null był o implementacje, które, biorąc pod uwagę , będzie pułapka na , ale nie będzie pułapka na anichar *p = NULL;*p=1234char *q = p+5678;*q = 1234;
Supercat

37
To po prostu cytat z Tony Hoare: en.wikipedia.org/wiki/Tony_Hoare#Apologies_and_retractions . Chociaż jestem pewien, że są chwile, kiedy nulljest to konieczne w arytmetyce wskaźników, zamiast tego interpretuję to, że powiedzenie, że arytmetyka wskaźników jest złym miejscem do hostowania semantyki twojego języka, nie oznacza to, że zero to nadal błąd.
J. Abrahamson,

18
@ superupat, możesz rzeczywiście napisać język bez wartości null. To, czy pozwolić na to, czy nie, jest wyborem.
Paul Draper,

6
@ superupat - ten problem istnieje również w Haskell, ale w innej formie. Haskell jest zwykle leniwy i niezmienny, a zatem pozwala pisać p = undefinedtak długo, jak pnie jest to oceniane. Bardziej pożytecznie, możesz wprowadzić undefinedzmienne odniesienie, o ile go nie ocenisz. Poważniejszym wyzwaniem są leniwe obliczenia, które mogą się nie kończyć, co oczywiście jest niezdecydowane. Główną różnicą jest to, że wszystkie jednoznacznie programują błędy i nigdy nie są używane do wyrażania zwykłej logiki.
Christian Conkle,

6
@supercat Haskell całkowicie nie ma semantyki odniesień (jest to pojęcie przejrzystości referencyjnej, która oznacza, że ​​wszystko jest zachowane przez zastąpienie odniesień ich odnośnikami). Dlatego myślę, że twoje pytanie jest źle postawione.
J. Abrahamson,

78
  • Wnioskowanie typu pełnego. Możesz właściwie używać złożonych typów bez odczuwania: „Kurcze, wszystko, co kiedykolwiek robię, to pisanie podpisów”.
  • Typy są w pełni algebraiczne , co bardzo ułatwia wyrażanie złożonych pomysłów.
  • Haskell ma klasy typów, które są swego rodzaju interfejsami, z tym wyjątkiem, że nie musisz umieszczać wszystkich implementacji dla jednego typu w tym samym miejscu. Możesz tworzyć implementacje własnych klas typów dla istniejących typów innych firm, bez potrzeby dostępu do ich źródła.
  • Funkcje wyższego rzędu i rekurencyjne mają tendencję do wkładania większej funkcjonalności w zakres funkcji sprawdzania typu. Weźmy na przykład filtr . W języku imperatywnym możesz napisać forpętlę, aby zaimplementować tę samą funkcjonalność, ale nie będziesz mieć takich samych gwarancji typu statycznego, ponieważ forpętla nie ma pojęcia typu zwracanego.
  • Brak podtypów znacznie upraszcza polimorfizm parametryczny.
  • Typy o większym doborze (typy typów) są stosunkowo łatwe do określenia i użycia w Haskell, co pozwala tworzyć abstrakcje wokół typów, które są całkowicie niezgłębione w Javie.

7
Ładna odpowiedź - czy możesz podać mi prosty przykład rodzaju wyższego rodzaju, pomyśl, że pomogłoby mi to zrozumieć, dlaczego nie można tego zrobić w java.
phatmanace

3
Istnieje kilka dobrych przykładów tutaj .
Karl Bielefeldt,

3
Dopasowanie wzorca jest również bardzo ważne, oznacza to, że możesz użyć typu obiektu, aby bardzo łatwo podejmować decyzje.
Benjamin Gruenbaum,

2
@BenjaminGruenbaum Nie sądzę, że nazwałbym to funkcją systemu typów.
Doval

3
Chociaż narzędzia ADT i HKT są zdecydowanie częścią odpowiedzi, wątpię, aby ktokolwiek zadający to pytanie dowie się, dlaczego są przydatne, sugeruję, aby obie sekcje zostały rozszerzone, aby to wyjaśnić
jk.

62
a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer

W Haskell: liczba całkowita, liczba całkowita, która może być zerowa, liczba całkowita, której wartość pochodzi ze świata zewnętrznego, oraz liczba całkowita, która może być łańcuchem, to wszystkie odrębne typy - i kompilator to wymusi . Nie można skompilować programu Haskell, który nie przestrzega tych różnic.

(Można jednak pominąć deklaracje typu. W większości przypadków kompilator może określić najbardziej ogólny typ zmiennych, co spowoduje pomyślną kompilację. Czy to nie fajne?)


11
+1, podczas gdy ta odpowiedź nie jest kompletna, myślę, że lepiej jest ustawić ją na poziomie pytania
jk.

1
+1 Chociaż pomogłoby to wyjaśnić, że inne języki również mają Maybe(np. Java Optionali Scala Option), ale w tych językach jest to rozwiązanie na wpół wypalone, ponieważ zawsze można przypisać nullzmienną tego typu i spowodować, że Twój program eksploduje podczas uruchamiania czas. Nie może się to zdarzyć w przypadku Haskell [1], ponieważ nie ma wartości zerowej , więc po prostu nie można oszukiwać. ([1]: w rzeczywistości można wygenerować błąd podobny do wyjątku NullPointerException przy użyciu funkcji częściowych, takich jak fromJustgdy masz Nothing, ale funkcje te są prawdopodobnie źle postrzegane ).
Andres F.

2
„liczba całkowita, której wartość pochodzi ze świata zewnętrznego” - nie IO Integerbyłby bliższy „podprogramowi, który po wykonaniu daje liczbę całkowitą”? Ponieważ a) main = c >> cwartość zwracana przez pierwszy cmoże być inna niż przez sekundę, cpodczas gdy abędzie miała tę samą wartość bez względu na jej pozycję (o ile jesteśmy w jednym zakresie) b) istnieją typy, które oznaczają wartości ze świata zewnętrznego w celu wymuszenia jego sanatyzacji (tj. nie umieszczać ich bezpośrednio, ale najpierw sprawdzić, czy dane wejściowe od użytkownika są prawidłowe / nie złośliwe).
Maciej Piechotka,

4
Maciej, to byłoby bardziej dokładne. Dążyłem do prostoty.
WolfeFan

30

Wiele osób wymieniło dobre rzeczy na temat Haskell. Ale w odpowiedzi na konkretne pytanie „dlaczego system typów poprawia działanie programów?”, Podejrzewam, że odpowiedzią jest „polimorfizm parametryczny”.

Rozważ następującą funkcję Haskell:

foobar :: x -> y -> y

Jest dosłownie tylko jeden możliwy sposób realizacji tej funkcji. Tylko po podpisie typu mogę dokładnie powiedzieć , co robi ta funkcja, ponieważ jest tylko jedna możliwa rzecz, którą może zrobić. [OK, niezupełnie, ale prawie!]

Zatrzymaj się i pomyśl o tym przez chwilę. To naprawdę wielka sprawa! Oznacza to, że jeśli napiszę funkcję z tym podpisem, nie jest możliwe, aby ta funkcja zrobiła coś innego niż zamierzałem. (Sama sygnatura typu może oczywiście być niepoprawna. Żaden język programowania nigdy nie zapobiegnie wszelkim błędom.)

Rozważ tę funkcję:

fubar :: Int -> (x -> y) -> y

Ta funkcja jest niemożliwa . Nie możesz dosłownie zaimplementować tej funkcji. Mogę to stwierdzić na podstawie podpisu typu.

Jak widać, sygnatura typu Haskell mówi ci naprawdę dużo!


Porównaj z C #. (Przepraszam, moja Java jest trochę zardzewiała.)

public static TY foobar<TX, TY>(TX in1, TY in2)

Metoda ta może zrobić kilka rzeczy:

  • Wróć in2jako wynik.
  • Pętla na zawsze i nigdy nic nie zwracaj.
  • Rzuć wyjątek i nigdy niczego nie zwracaj.

W rzeczywistości Haskell ma również te trzy opcje. Ale C # daje również dodatkowe opcje:

  • Zwróć null. (Haskell nie ma wartości null).
  • Zmodyfikuj in2przed zwrotem. (Haskell nie ma modyfikacji na miejscu).
  • Użyj refleksji. (Haskell nie ma odbicia).
  • Wykonaj wiele operacji we / wy przed zwróceniem wyniku. (Haskell nie pozwoli Ci wykonać operacji wejścia / wyjścia, chyba że zadeklarujesz, że wykonujesz operacje wejścia / wyjścia tutaj.)

Odbicie jest szczególnie dużym młotem; za pomocą odbicia mogę zbudować nowy TYobiekt z cienkiego powietrza i zwrócić go! Mogę sprawdzić oba obiekty i wykonywać różne czynności w zależności od tego, co znajdę. Mogę dokonywać dowolnych modyfikacji obu przekazywanych obiektów.

I / O jest podobnie dużym młotem. Kod może wyświetlać komunikaty dla użytkownika, otwierać połączenia z bazą danych lub formatować dysk twardy lub cokolwiek innego.


Natomiast foobarfunkcja Haskell może pobierać tylko niektóre dane i zwracać te dane bez zmian. Nie może „patrzeć” na dane, ponieważ ich typ jest nieznany w czasie kompilacji. Nie może tworzyć nowych danych, ponieważ ... cóż, jak konstruujesz dane dowolnego możliwego typu? Potrzebujesz do tego refleksji. Nie może wykonywać żadnych operacji we / wy, ponieważ podpis typu nie deklaruje wykonywania operacji we / wy. Więc nie może wchodzić w interakcje z systemem plików lub siecią, ani nawet uruchamiać wątków w tym samym programie! (Tj. Jest w 100% gwarantowany wątek.)

Jak widać, nie pozwalając ci robić wielu rzeczy, Haskell pozwala ci na bardzo mocne gwarancje tego, co faktycznie robi twój kod. Tak ścisłe, że (dla naprawdę kodu polimorficznego) zwykle jest tylko jeden możliwy sposób, aby elementy pasowały do ​​siebie.

(Żeby było jasne: nadal można pisać funkcje Haskella, w których sygnatura typu niewiele mówi. Int -> IntMoże to być wszystko. Ale nawet wtedy wiemy, że to samo wejście zawsze będzie generować takie samo wyjście ze 100% pewnością. Java nawet tego nie gwarantuje!)


4
+1 Świetna odpowiedź! Jest to bardzo potężne i często niedoceniane przez przybyszów do Haskell. Nawiasem mówiąc, prostsza funkcja „niemożliwa” byłaby fubar :: a -> b, prawda? (Tak, jestem tego świadomy unsafeCoerce. Zakładam, że nie mówimy o niczym z „niebezpiecznym” w nazwie, i nowi przybysze nie powinni się tym martwić!: D)
Andres F.

Tak, jest wiele prostszych podpisów, których nie można napisać. Na przykład foobar :: xjest dość niemożliwy do wdrożenia ...
MathematicalOrchid

W rzeczywistości nie można sprawić, by czysty kod był niebezpieczny dla wątków, ale nadal można uczynić go wielowątkowym. Masz następujące opcje: „zanim to ocenisz, oceń”, „kiedy to ocenisz, możesz chcieć to również ocenić w osobnym wątku” oraz „kiedy to ocenisz, możesz również to ocenić, być może w osobnym wątku ". Domyślnie jest to „rób, co chcesz”, co zasadniczo oznacza „oceniaj tak późno, jak to możliwe”.
John Dvorak,

Bardziej prozaicznie możesz wywoływać metody instancji na in1 lub in2, które mają skutki uboczne. Lub możesz zmodyfikować stan globalny (który, oczywiście, jest modelowany jako działanie IO w Haskell, ale może nie być tym, co większość ludzi myśli o IO).
Doug McClean,

2
@ isomorphismes Typ x -> y -> ymożna doskonale wdrożyć. Typ (x -> y) -> ynie jest. Typ x -> y -> ypobiera dwa dane wejściowe i zwraca drugie. Typ (x -> y) -> yprzyjmuje funkcję, która działa x, i jakoś musi yz tego
MathematicalOrchid

17

Powiązane pytanie SO .

Zakładam, że możesz zrobić to samo w haskell (tj. Wstawiać rzeczy w ciągi / ints, które powinny być naprawdę typami danych pierwszej klasy)

Nie, naprawdę nie możesz - przynajmniej nie w taki sam sposób jak Java. W Javie dzieje się coś takiego:

String x = (String)someNonString;

a Java z przyjemnością spróbuje rzucić nie-Ciąg jako Ciąg. Haskell nie pozwala na tego typu rzeczy, eliminując całą klasę błędów w czasie wykonywania.

nulljest częścią systemu typów (as Nothing), więc należy wyraźnie o to poprosić i obsłużyć go, eliminując całą inną klasę błędów środowiska wykonawczego.

Istnieje również wiele innych subtelnych korzyści - zwłaszcza w zakresie ponownego użycia i klas typów - których nie mam wystarczającej wiedzy, aby się komunikować.

Głównie dlatego, że system typów Haskell pozwala na dużą ekspresję. Możesz zrobić wiele rzeczy na kilku zasadach. Rozważ zawsze obecne drzewo Haskell:

data Tree a = Leaf a | Branch (Tree a) (Tree a) 

Zdefiniowałeś całe ogólne drzewo binarne (i dwa konstruktory danych) w dość czytelnym jednym wierszu kodu. Wszystko to przy użyciu kilku reguł (posiadających typy sum i typy produktów ). To jest 3-4 pliki kodu i klasy w Javie.

Ten rodzaj zwięzłości / elegancji jest szczególnie ceniony szczególnie wśród osób skłonnych do szanowania systemów typu.


Z twojej odpowiedzi zrozumiałem tylko wyjątki NullPointerException. Czy możesz podać więcej przykładów?
Jesvin Jose

2
Niekoniecznie prawda, JLS §5.5.1 : Jeśli T jest typem klasy, to albo | S | <: | T | lub | T | <: | S |. W przeciwnym razie wystąpi błąd kompilacji. Kompilator nie pozwoli więc na rzucanie niewymienialnych typów - istnieją oczywiście sposoby na ich obejście.
Boris the Spider

Moim zdaniem najprostszym sposobem na wykorzystanie zalet klas typów jest to, interfaceże można je dodać po fakcie i nie „zapominają” typu, który je implementuje. Oznacza to, że możesz upewnić się, że dwa argumenty funkcji mają ten sam typ, w przeciwieństwie do interfaces, gdzie dwa List<String>s mogą mieć różne implementacje. Możesz technicznie zrobić coś bardzo podobnego w Javie, dodając parametr type do każdego interfejsu, ale 99% istniejących interfejsów tego nie robi i do diabła zmylisz swoich rówieśników.
Doval

2
@ BoristheSpider Prawda, ale wyjątki rzutowania prawie zawsze obejmują downcasting z nadklasy do podklasy lub z interfejsu do klasy, i nie jest niczym niezwykłym, że może być Object.
Doval

2
Myślę, że pytanie o ciągi nie ma związku z błędami rzutowania i typami wykonawczymi, ale faktem, że jeśli nie chcesz używać typów, Java nie zmusi cię - tak jak w rzeczywistości - do przechowywania danych w postaci szeregowej forma, nadużywanie ciągów jako typu ad-hoc any. Haskell też cię nie powstrzyma, ponieważ ... cóż, ma sznurki. Haskell może dać ci narzędzia, nie może siłą powstrzymać cię przed robieniem głupich rzeczy, jeśli nalegasz, aby Greenspunning był wystarczająco tłumaczem, aby wymyślić na nowo nullw zagnieżdżonym kontekście. Żaden język nie może.
Leushenko,

0

Jednym z „memów”, o których często go znają ludzie, jest cała rzecz „jeśli się skompiluje, zadziała *” - co moim zdaniem jest związane z siłą systemu typów.

Dotyczy to głównie małych programów. Haskell zapobiega popełnianiu błędów, które są łatwe w innych językach (np. Porównywanie an Int32i a, Word32a coś eksploduje), ale nie chroni przed wszystkimi błędami.

Haskell znacznie ułatwia refaktoryzację . Jeśli twój program był wcześniej poprawny i sprawdza typ, istnieje spora szansa, że ​​nadal będzie poprawny po drobnych poprawkach.

Próbuję zrozumieć, dlaczego właśnie Haskell jest lepszy niż inne statycznie pisane języki pod tym względem.

Typy w Haskell są dość lekkie, ponieważ łatwo jest zadeklarować nowe typy. Jest to w przeciwieństwie do języka takiego jak Rust, w którym wszystko jest nieco bardziej kłopotliwe.

Zakładam, że to właśnie ludzie rozumieją przez silny system typów, ale nie jest dla mnie oczywiste, dlaczego Haskell jest lepszy.

Haskell ma wiele funkcji poza prostymi typami sum i produktów; ma również typy uniwersalne (np. id :: a -> a). Możesz także tworzyć typy rekordów zawierające funkcje, które są zupełnie inne niż język taki jak Java lub Rust.

GHC może również wyprowadzać niektóre instancje na podstawie samych typów, a od pojawienia się ogólnych, możesz pisać funkcje, które są ogólne dla różnych typów. Jest to dość wygodne i bardziej płynne niż to samo w Javie.

Inna różnica polega na tym, że Haskell ma stosunkowo dobre błędy pisowni (przynajmniej w pisaniu). Wnioskowanie typu Haskell jest wyrafinowane i dość rzadko trzeba podać adnotacje typu, aby uzyskać kompilację. Jest to w przeciwieństwie do Rust, gdzie wnioskowanie na temat typu może czasem wymagać adnotacji, nawet jeśli kompilator może w zasadzie wydedukować typ.

Wreszcie Haskell ma klasy, w tym słynną monadę. Monady są szczególnie dobrym sposobem radzenia sobie z błędami; w zasadzie dają ci prawie całą wygodę - nullbez okropnego debugowania i bez rezygnacji z własnego bezpieczeństwa. Zatem możliwość pisania funkcji na tych typach naprawdę ma duże znaczenie, jeśli chodzi o zachęcanie nas do korzystania z nich!

Innymi słowy, możesz pisać dobrą lub złą Javę, zakładam, że możesz zrobić to samo w Haskell

Być może to prawda, ale brakuje w tym kluczowego punktu: punkt, w którym zaczynasz strzelać sobie w stopę w Haskell, jest dalej niż punkt, w którym zaczynasz strzelać sobie w stopę w Javie.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.