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 string
zawiera elephant, giraffe
, itp., A jeśli ktoś w to Mercedes
włą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ń!
źródło
Maybe
tylko 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ę.Odpowiedzi:
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)
Eq
kompilator 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.data Bt a = Here a | There (Bt (a, a))
. Pomyśl dokładnie o prawidłowych wartościachBt a
i zwróć uwagę, jak działa ten typ. To trudne!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.mtl
system 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.(+)
razem działać? OchInteger
, ok! Teraz wstawmy odpowiedni kod!”. W bardziej złożonych systemach możesz tworzyć bardziej interesujące ograniczenia.mtl
biblioteka oparta jest na tym pomyśle.(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ż, żea
zmienna jest związana tylko w argumencie. Oznacza to, że osoba definiująca funkcjęmapFree
decyduje o tym,a
w której chwili jest tworzona instancja, a nie użytkownikmapFree
.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
State
jest 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, takichHandle
jak przyjmowanie argumentów dla określonych rodzajów, takich jakState
. Rozumienie wszystkich szczegółów jest mylące, ale także bardzo poprawne.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 -> TypeRepr
to 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 .Typeable
System Haskell pozwala stworzyć sejfcoerce :: (Typeable a, Typeable b) => a -> Maybe b
. Ten system opiera się naTypeable
implementacji 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.
IO a
który jest reprezentowany obliczenia uboczne, których wynikiem są wartości typua
. To podstawa bardzo ładnego systemu efektów osadzonego w czystym języku.null
. Wszyscy wiedzą, żenull
jest 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 typuA
w typMaybe A
, całkowicie łagodzą problemnull
.Bt a
typu sprzed i spróbuj napisać funkcję do obliczenia jego wielkość:size :: Bt a -> Int
. Będzie to wyglądać trochę jaksize (Here a) = 1
isize (There bt) = 2 * size bt
. Operacyjnie nie jest to zbyt skomplikowane, ale zauważ, że rekurencyjne wywołaniesize
w ostatnim równaniu występuje w innym typie , ale ogólna definicja ma ładny ogólny typsize :: 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ę.
źródło
char *p = NULL;
*p=1234
char *q = p+5678;
*q = 1234;
null
jest 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.p = undefined
tak długo, jakp
nie jest to oceniane. Bardziej pożytecznie, możesz wprowadzićundefined
zmienne 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.for
pętlę, aby zaimplementować tę samą funkcjonalność, ale nie będziesz mieć takich samych gwarancji typu statycznego, ponieważfor
pętla nie ma pojęcia typu zwracanego.źródło
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?)
źródło
Maybe
(np. JavaOptional
i ScalaOption
), ale w tych językach jest to rozwiązanie na wpół wypalone, ponieważ zawsze można przypisaćnull
zmienną 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 jakfromJust
gdy maszNothing
, ale funkcje te są prawdopodobnie źle postrzegane ).IO Integer
byłby bliższy „podprogramowi, który po wykonaniu daje liczbę całkowitą”? Ponieważ a)main = c >> c
wartość zwracana przez pierwszyc
może być inna niż przez sekundę,c
podczas gdya
bę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).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:
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ę:
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.)
Metoda ta może zrobić kilka rzeczy:
in2
jako wynik.W rzeczywistości Haskell ma również te trzy opcje. Ale C # daje również dodatkowe opcje:
in2
przed zwrotem. (Haskell nie ma modyfikacji na miejscu).Odbicie jest szczególnie dużym młotem; za pomocą odbicia mogę zbudować nowy
TY
obiekt 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
foobar
funkcja 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 -> Int
Moż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!)źródło
fubar :: a -> b
, prawda? (Tak, jestem tego świadomyunsafeCoerce
. Zakładam, że nie mówimy o niczym z „niebezpiecznym” w nazwie, i nowi przybysze nie powinni się tym martwić!: D)foobar :: x
jest dość niemożliwy do wdrożenia ...x -> y -> y
można doskonale wdrożyć. Typ(x -> y) -> y
nie jest. Typx -> y -> y
pobiera dwa dane wejściowe i zwraca drugie. Typ(x -> y) -> y
przyjmuje funkcję, która działax
, i jakoś musiy
z tegoPowiązane pytanie SO .
Nie, naprawdę nie możesz - przynajmniej nie w taki sam sposób jak Java. W Javie dzieje się coś takiego:
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.
null
jest częścią systemu typów (asNothing
), 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:
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.
źródło
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 dointerface
s, gdzie dwaList<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.Object
.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 nowonull
w zagnieżdżonym kontekście. Żaden język nie może.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
Int32
i a,Word32
a 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.
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.
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ę -
null
bez 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!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.
źródło