Tak, bardzo proste pytanie na pozór. Ale jeśli poświęcisz czas na przemyślenie go do końca, dostaniesz się w niezmierzoną głębię teorii typów. Teoria typów wpatruje się również w ciebie.
Po pierwsze, oczywiście poprawnie zorientowałeś się, że F # nie ma klas typów i dlatego. Ale proponujesz interfejs Mappable
. Ok, spójrzmy na to.
Powiedzmy, że możemy zadeklarować taki interfejs. Czy możesz sobie wyobrazić, jak wyglądałby jego podpis?
type Mappable =
abstract member map : ('a -> 'b) -> 'f<'a> -> 'f<'b>
Gdzie f
jest typ implementujący interfejs. Zaczekaj! F # też tego nie ma! Oto f
zmienna typu wyższego rodzaju, a F # wcale nie ma wyższej życzliwości. Nie ma możliwości zadeklarowania funkcji f : 'm<'a> -> 'm<'b>
ani czegoś takiego.
Ale ok, powiedzmy, że pokonaliśmy również tę przeszkodę. A teraz mamy interfejs Mappable
, który może być realizowany przez List
, Array
, Seq
i zlewozmywakiem. Ale poczekaj! Teraz mamy metodę zamiast funkcji, a metody nie komponują się dobrze! Spójrzmy na dodanie 42 do każdego elementu listy zagnieżdżonej:
// Good ol' functions:
add42 nestedList = nestedList |> List.map (List.map ((+) 42))
// Using an interface:
add42 nestedList = nestedList.map (fun l -> l.map ((+) 42))
Spójrz: teraz musimy użyć wyrażenia lambda! Nie ma sposobu, aby to przekazać.map
implementacji do innej funkcji jako wartości. Skutecznie koniec „funkcji jako wartości” (i tak, wiem, użycie lambda nie wygląda bardzo źle w tym przykładzie, ale zaufaj mi, robi się bardzo brzydkie)
Ale czekaj, jeszcze nie skończyliśmy. Teraz, gdy jest to wywołanie metody, wnioskowanie typu nie działa! Ponieważ podpis typu metody .NET zależy od typu obiektu, kompilator nie może wywnioskować obu. Jest to w rzeczywistości bardzo częsty problem, na który napotykają początkujący użytkownicy podczas współpracy z bibliotekami .NET. Jedynym lekarstwem jest podanie podpisu typu:
add42 (nestedList : #Mappable) = nestedList.map (fun l -> l.map ((+) 42))
Och, ale to wciąż nie wystarczy! Mimo że podałem nestedList
sam sobie podpis , nie podałem podpisu parametru lambda l
. Jaki powinien być taki podpis? Czy powiedziałbyś, że tak powinno być fun (l: #Mappable) -> ...
? Aha, a teraz w końcu dotarliśmy do typów rangi N, jak widzicie, #Mappable
jest skrótem do „dowolnego typu 'a
takiego, że 'a :> Mappable
” - tj. Wyrażenia lambda, które samo jest ogólne.
Lub, alternatywnie, możemy wrócić do wyższej życzliwości i nestedList
dokładniej określić rodzaj :
add42 (nestedList : 'f<'a<'b>> where 'f :> Mappable, 'a :> Mappable) = ...
Ale ok, odłóżmy na razie wnioskowanie o typie i wróćmy do wyrażenia lambda i tego, jak nie możemy teraz przekazać map
jako wartości do innej funkcji. Powiedzmy, że nieco rozszerzyliśmy składnię, aby umożliwić coś takiego, co Elm robi z polami rekordów:
add42 nestedList = nestedList.map (.map ((+) 42))
Jaki by to był typ .map
? Musiałby to być typ ograniczony , tak jak w Haskell!
.map : Mappable 'f => ('a -> 'b) -> 'f<'a> -> 'f<'b>
Wow ok. Odkładając na bok fakt, że .NET nawet nie pozwala na istnienie takich typów, w rzeczywistości właśnie przywróciliśmy klasy typów!
Ale jest powód, dla którego F # nie ma klas typów. Wiele aspektów tego powodu opisano powyżej, ale bardziej zwięzły sposób to: prostota .
Widzisz, to jest kula przędzy. Kiedy już masz klasy typów, musisz mieć ograniczenia, wyższą życzliwość, rangę N (lub przynajmniej rangę 2), a zanim się zorientujesz, poprosisz o impredykatywne typy, funkcje pisania, GADT i wszystkie reszta.
Ale Haskell płaci cenę za wszystkie gadżety. Okazuje się, że nie ma dobrego sposobu na wywnioskowanie tego wszystkiego. Typy o wyższych typach działają, ale ograniczenia już nie. Ranga N - nawet o tym nie marzysz. I nawet gdy to działa, pojawiają się błędy pisowni, które musisz mieć doktoratu, aby zrozumieć. I dlatego w Haskell jesteś delikatnie zachęcany do umieszczania podpisów na wszystkim. Cóż, nie wszystko - wszystko , ale naprawdę prawie wszystko. A tam, gdzie nie umieszczasz podpisów tekstowych (np. Wewnątrz let
i where
) - niespodzianka-niespodzianka, te miejsca są tak naprawdę monomorfizowane, więc w zasadzie powrócisz do uproszczonej wersji F #.
Z drugiej strony w języku F # podpisy typów są rzadkie, głównie do dokumentacji lub do współpracy .NET. Poza tymi dwoma przypadkami możesz napisać cały duży złożony program w języku F # i nie używać podpisu typu jeden raz. Wnioskowanie typu działa dobrze, ponieważ nie ma w nim nic zbyt skomplikowanego lub niejednoznacznego.
I to jest duża przewaga F # nad Haskell. Tak, Haskell pozwala wyrażać bardzo złożone rzeczy w bardzo precyzyjny sposób, to dobrze. Ale F # pozwala ci być bardzo prymitywnym, prawie jak Python lub Ruby, a kompilator cię złapie, jeśli się potkniesz.