Typy egzystencjalne nie są tak naprawdę uważane za złą praktykę w programowaniu funkcjonalnym. Myślę, że tym, co cię denerwuje , jest to, że jednym z najczęściej cytowanych zastosowań egzystencjalnych jest egzystencjalny antytyp typu egzystencjalnego , który zdaniem wielu osób jest złą praktyką.
Ten wzorzec jest często wykreślany jako odpowiedź na pytanie, w jaki sposób mieć listę heterogenicznie typowanych elementów, które wszystkie implementują tę samą klasę. Na przykład możesz chcieć mieć listę wartości, które mają Show
instancje:
{-# LANGUAGE ExistentialTypes #-}
class Shape s where
area :: s -> Double
newtype Circle = Circle { radius :: Double }
instance Shape Circle where
area (Circle r) = pi * r^2
newtype Square = Square { side :: Double }
area (Square s) = s^2
data AnyShape = forall x. Shape x => AnyShape x
instance Shape AnyShape where
area (AnyShape x) = area x
example :: [AnyShape]
example = [AnyShape (Circle 1.0), AnyShape (Square 1.0)]
Problem z takim kodem jest następujący:
- Jedyną przydatną operacją, jaką możesz wykonać na,
AnyShape
jest zdobycie jego obszaru.
- Nadal musisz użyć
AnyShape
konstruktora, aby wprowadzić jeden z typów kształtów do tego AnyShape
typu.
Jak się okazuje, ten fragment kodu tak naprawdę nie daje ci niczego, czego ten krótszy nie:
class Shape s where
area :: s -> Double
newtype Circle = Circle { radius :: Double }
instance Shape Circle where
area (Circle r) = pi * r^2
newtype Square = Square { side :: Double }
area (Square s) = s^2
example :: [Double]
example = [area (Circle 1.0), area (Square 1.0)]
W przypadku klas opartych na wielu metodach ten sam efekt można ogólnie osiągnąć prościej, stosując kodowanie „rekord metod” - zamiast typowej klasy Shape
definiuje się typ rekordu, którego polami są „metody” danego Shape
typu , i piszesz funkcje do przekształcania kręgów i kwadratów w Shape
s.
Ale to nie znaczy, że typy egzystencjalne są problemem! Na przykład w Rust mają one cechę zwaną obiektami cech, które ludzie często opisują jako cechy egzystencjalne nad cechą (wersje klas typu Rust). Jeśli egzystencjalne typy znaków są antypatternem w Haskell, czy to oznacza, że Rust wybrał złe rozwiązanie? Nie! Motywacja w świecie Haskell dotyczy składni i wygody, a nie zasady.
Bardziej matematycznym sposobem na przedstawienie tego jest wskazanie, że AnyShape
typ z góry i Double
są izomorficzne - istnieje między nimi „bezstratna konwersja” (no, z wyjątkiem precyzji zmiennoprzecinkowej):
forward :: AnyShape -> Double
forward = area
backward :: Double -> AnyShape
backward x = AnyShape (Square (sqrt x))
Mówiąc ściśle, nie zyskujesz ani nie tracisz żadnej mocy, wybierając jedną z nich. Co oznacza, że wybór powinien opierać się na innych czynnikach, takich jak łatwość użycia lub wydajność.
I pamiętaj, że typy egzystencjalne mają inne zastosowania poza tym przykładem list heterogenicznych, więc dobrze jest je mieć. Na przykład ST
typ Haskella , który pozwala nam pisać funkcje, które są zewnętrznie czyste, ale wewnętrznie wykorzystują operacje mutacji pamięci, wykorzystuje technikę opartą na typach egzystencjalnych w celu zagwarantowania bezpieczeństwa w czasie kompilacji.
Ogólna odpowiedź jest taka, że nie ma ogólnej odpowiedzi. Użycie typów egzystencjalnych można oceniać tylko w kontekście - a odpowiedzi mogą być różne w zależności od tego, jakie funkcje i składnia są dostępne w różnych językach.