Dlaczego (a dlaczego nie) typy egzystencjalne są uważane za złą praktykę w programowaniu funkcjonalnym?

43

Jakich technik mogę użyć, aby konsekwentnie refaktoryzować kod, usuwając zależność od typów egzystencjalnych? Zazwyczaj są one używane do dyskwalifikacji niepożądanych konstrukcji twojego typu, a także do umożliwienia konsumpcji przy minimalnej wiedzy na temat danego typu (przynajmniej tak rozumiem).

Czy ktoś wymyślił prosty, spójny sposób na usunięcie polegania na nich w kodzie, który nadal zapewnia pewne korzyści? A przynajmniej jakieś sposoby na wślizgnięcie się w abstrakcję, która pozwala na ich usunięcie bez konieczności znacznej rezygnacji z kodu, aby poradzić sobie ze zmianą?

Możesz przeczytać więcej o typach egzystencjalnych tutaj („jeśli odważysz się…”).

Petr Pudlák
źródło
8
@RobertHarvey Po raz pierwszy znalazłem post na blogu, który zmusił mnie do zastanowienia się nad tym pytaniem: Haskell Antipattern: Existential Typeclass . Ponadto w artykule, o którym wspomina Joey Adams, opisano niektóre poblemy z egzystencjalnymi w części 3.1. Jeśli masz sprzeczne argumenty, udostępnij je.
Petr Pudlák
5
@ PetrPudlák: Należy pamiętać, że antatattern nie ma w ogóle typów egzystencjalnych, ale ich szczególne zastosowanie, gdy coś prostszego i łatwiejszego (i lepiej obsługiwanego w Haskell) wykonałoby tę samą pracę.
CA McCann
10
Jeśli chcesz wiedzieć, dlaczego autor konkretnego postu na blogu wyraził opinię, osoba, którą prawdopodobnie powinieneś zapytać, jest autorem.
Eric Lippert,
6
@Ptolemy To kwestia opinii. Używając Haskell od lat, nie mogę sobie wyobrazić używania funkcjonalnego języka, który nie ma silnego systemu pisma.
Petr Pudlák
4
@Ptolemy: Mantra Haskella brzmi: „jeśli się skompiluje, działa”, mantra Erlanga brzmi „niech się rozbije”. Pisanie dynamiczne jest dobre jako klej, ale osobiście nie budowałbym rzeczy za pomocą samego kleju.
Den

Odpowiedzi:

8

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ą Showinstancje:

{-# 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:

  1. Jedyną przydatną operacją, jaką możesz wykonać na, AnyShapejest zdobycie jego obszaru.
  2. Nadal musisz użyć AnyShapekonstruktora, aby wprowadzić jeden z typów kształtów do tego AnyShapetypu.

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 Shapedefiniuje się typ rekordu, którego polami są „metody” danego Shapetypu , i piszesz funkcje do przekształcania kręgów i kwadratów w Shapes.


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 AnyShapetyp z góry i Doubleizomorficzne - 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 STtyp 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.

sacundim
źródło
To nie jest izomorfizm.
Ryan Reich
2

Nie znam się zbyt dobrze na Haskell, więc postaram się odpowiedzieć na ogólną część pytania jako nieakademicki funkcjonalny programista C #.

Po przeczytaniu okazuje się, że:

  1. Symbole wieloznaczne Java są podobne do typów egzystencjalnych:

    Różnica między typami egzystencjalnymi Scali a symbolem wieloznacznym Javy na przykładzie

  2. Symbole wieloznaczne nie są całkowicie zaimplementowane w języku C #: wariancja ogólna jest obsługiwana, ale wariancja witryny wywoływania nie jest

    C # Generics: Symbole wieloznaczne

  3. Możesz nie potrzebować tej funkcji codziennie, ale kiedy ją poczujesz (np. Musisz wprowadzić dodatkowy typ, aby wszystko działało):

    Symbole wieloznaczne w ogólnych ograniczeniach C #

Na podstawie tych informacji typy egzystencjalne / symbole wieloznaczne są użyteczne, jeśli są poprawnie zaimplementowane i nie ma w nich nic złego jako takiego, ale prawdopodobnie można je niewłaściwie wykorzystywać, tak jak inne funkcje językowe.

Legowisko
źródło