Dlaczego jest tyle funkcji `map` dla różnych typów w F #

9

Uczę się F #. Zacząłem FP z Haskellem i jestem tego ciekawy.

Ponieważ F # jest językiem .NET, wydaje mi się, że rozsądniej jest zadeklarować interfejs podobny do klasy typu Mappablehaskell Functor.

wprowadź opis zdjęcia tutaj

Ale, jak na powyższym obrazku, funkcje F # są oddzielne i realizowane samodzielnie. Jaki jest cel takiego projektu? Dla mnie wprowadzenie Mappable.mapi wdrożenie tego dla każdego typu danych byłoby wygodniejsze.

MyBug18
źródło
To pytanie nie należy do SO. To nie jest problem z programowaniem. Sugeruję, aby zapytać na F # Slack lub innym forum dyskusyjnym.
Bent Tranberg
5
@BentTranberg Hojnie przeczytany, The community is here to help you with specific coding, algorithm, or language problems.obejmowałby również pytania dotyczące projektowania języka, o ile inne kryteria są spełnione.
kaefer
3
Krótko mówiąc, F # nie ma klas typów, a zatem musi ponownie wdrożyć mapi inne wspólne funkcje wyższego rzędu dla każdego typu kolekcji. Interfejs niewiele by pomógł, ponieważ nadal wymaga, aby każdy typ kolekcji zapewniał osobną implementację.
dumetrulo

Odpowiedzi:

19

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 fjest typ implementujący interfejs. Zaczekaj! F # też tego nie ma! Oto fzmienna 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, Seqi 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 nestedListsam 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, #Mappablejest skrótem do „dowolnego typu 'atakiego, ż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 nestedListdokł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ć mapjako 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 leti 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.

Fiodor Soikin
źródło