Myślę, że polimorfizm typu zwracanego jest jedną z najlepszych cech klas typów. Po dłuższym użyciu czasami ciężko mi wrócić do modelowania w stylu OOP, w którym go nie mam.
Rozważ kodowanie algebry. W Haskell mamy klasę typów Monoid
(ignorowanie mconcat
)
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Jak możemy zakodować to jako interfejs w języku OO? Krótka odpowiedź brzmi: nie możemy. To dlatego, że typem mempty
jest (Monoid a) => a
aka, polimorfizm typu zwracanego. Możliwość modelowania algebry jest niezwykle przydatna w IMO. *
Zaczynasz swój post od skargi dotyczącej „Przejrzystości referencyjnej”. Rodzi to ważną kwestię: Haskell jest językiem zorientowanym na wartości. Wyrażenia takie jak read 3
nie muszą być rozumiane jako rzeczy, które obliczają wartości, mogą być również rozumiane jako wartości. Oznacza to, że prawdziwym problemem nie jest polimorfizm typu powrotu: są to wartości typu polimorficznego ( []
i Nothing
). Jeśli język powinien je mieć, to tak naprawdę musi mieć polimorficzne typy zwracania dla zachowania spójności.
Czy powinniśmy być w stanie powiedzieć, że []
jest typu forall a. [a]
? Chyba tak. Te funkcje są bardzo przydatne i znacznie upraszczają język.
Gdyby Haskell miał podtyp, polimorfizm []
mógłby być podtypem dla wszystkich [a]
. Problem polega na tym, że nie znam sposobu kodowania, który bez typu pustej listy byłby polimorficzny. Zastanów się, jak można to zrobić w Scali (jest to krótsze niż w kanonicznym języku OOP o typie statycznym, Java)
abstract class List[A]
case class Nil[A] extends List[A]
case class Cons[A](h: A. t: List[A]) extends List[A]
Nawet tutaj Nil()
jest obiektem typu Nil[A]
**
Kolejną zaletą polimorfizmu typu powrotu jest to, że sprawia, że osadzanie Curry-Howarda jest znacznie prostsze.
Rozważ następujące logiczne twierdzenia:
t1 = forall P. forall Q. P -> P or Q
t2 = forall P. forall Q. P -> Q or P
Możemy w prosty sposób uchwycić je jako twierdzenia w Haskell:
data Either a b = Left a | Right b
t1 :: a -> Either a b
t1 = Left
t2 :: a -> Either b a
t2 = Right
Podsumowując: lubię polimorfizm typu zwracanego i uważam, że łamie on przejrzystość referencyjną, jeśli masz ograniczone pojęcie wartości (chociaż jest to mniej przekonujące w przypadku klasy typu ad hoc). Z drugiej strony, znajduję twoje uwagi na temat MR i wpisuję domyślnie przekonujące.
*. W komentarzach ysdx wskazuje, że nie jest to do końca prawdą: moglibyśmy ponownie zaimplementować klasy typów, modelując algebrę jako inny typ. Jak java:
abstract class Monoid<M>{
abstract M empty();
abstract M append(M m1, M m2);
}
Następnie musisz przekazać ze sobą obiekty tego typu. Scala ma pojęcie niejawnych parametrów, które pozwalają uniknąć niektórych, ale z mojego doświadczenia nie wszystkich, kosztów związanych z jawnym zarządzaniem tymi rzeczami. Umieszczenie metod użyteczności (metody fabryczne, metody binarne itp.) Na osobnym typie ograniczonym literą F okazuje się być niewiarygodnie dobrym sposobem zarządzania rzeczami w języku OO, który ma wsparcie dla generycznych. To powiedziawszy, nie jestem pewien, czy wymyśliłbym ten wzór, gdybym nie miał doświadczenia w modelowaniu rzeczy za pomocą klas, i nie jestem pewien, czy inni to zrobią.
Ma również ograniczenia, po wyjęciu z pudełka nie ma sposobu na uzyskanie obiektu, który implementuje klasę dla dowolnego typu. Musisz albo przekazać wartości jawnie, użyć czegoś takiego jak implikacje Scali, lub zastosować jakąś formę technologii wstrzykiwania zależności. Życie staje się brzydkie. Z drugiej strony fajnie jest mieć wiele implementacji dla tego samego typu. Coś może być monoidą na wiele sposobów. Ponadto noszenie tych struktur oddzielnie ma dla IMO bardziej matematycznie nowoczesny, konstruktywny charakter. Tak więc, chociaż generalnie wolę sposób Haskella, prawdopodobnie przesadziłem z moją sprawą.
Klasy typów z polimorfizmem typu zwrotnego sprawiają, że jest to łatwe w obsłudze. To nie oznacza, że to najlepszy sposób, aby to zrobić.
**. Jörg W Mittag podkreśla, że tak naprawdę nie jest to kanoniczny sposób robienia tego w Scali. Zamiast tego zastosowalibyśmy standardową bibliotekę z czymś bardziej podobnym do:
abstract class List[+A] ...
case class Cons[A](head: A, tail: List[A]) extends List[A] ...
case object Nil extends List[Nothing] ...
Wykorzystuje to wsparcie Scali dla typów dennych, a także paramaterii typu kowariantnego. Tak, Nil
jest typu Nil
nie Nil[A]
. W tym momencie jesteśmy dość daleko od Haskell, ale warto zauważyć, że Haskell reprezentuje typ dna
undefined :: forall a. a
Oznacza to, że nie jest podtypem wszystkich typów, jest polimorficznie (sp) członkiem wszystkich typów.
Jeszcze więcej polimorfizmu typu zwrotnego.