Dobry ogólny system typów

29

Powszechnie przyjmuje się, że niektóre rodzaje Java zawiodły w ważnych sprawach. Połączenie symboli wieloznacznych i granic spowodowało powstanie poważnie nieczytelnego kodu.

Jednak gdy patrzę na inne języki, naprawdę nie mogę znaleźć ogólnego systemu typów, z którego programiści są zadowoleni.

Jeśli weźmiemy pod uwagę następujące cele projektowania takiego systemu typów:

  • Zawsze tworzy czytelne deklaracje typu
  • Łatwy do nauczenia (nie trzeba odkurzać kowariancji, kontrawariancji itp.)
  • maksymalizuje liczbę błędów czasu kompilacji

Czy jest jakiś język, który dobrze to zrozumiał? Jeśli google, jedyne, co widzę, to skargi na to, jak system typów zasysa język X. Czy tego rodzaju złożoność jest nieodłącznym elementem typowego pisania? Czy powinniśmy po prostu zrezygnować z próby sprawdzenia bezpieczeństwa typu 100% w czasie kompilacji?

Moje główne pytanie dotyczy tego, który język najlepiej „poprawił” te trzy cele. Zdaję sobie sprawę, że to subiektywne, ale jak dotąd nie mogę nawet znaleźć jednego języka, w którym nie wszyscy programiści zgadzają się, że ogólny system typów jest bałaganem.

Dodatek: jak wspomniano, kombinacja podtypu / dziedziczenia i ogólnych jest tym, co tworzy złożoność, więc naprawdę szukam języka, który łączy oba te elementy i pozwala uniknąć eksplozji złożoności.

Piotr
źródło
2
Co masz na myśli easy-to-read type declarations? Trzecie kryterium jest również niejednoznaczne: na przykład mogę przekształcić indeks tablicy poza wyjątkami granic w błędy czasu kompilacji, nie pozwalając na indeksowanie tablic, chyba że będę mógł obliczyć indeks w czasie kompilacji. Również drugie kryterium wyklucza podtypowanie. To niekoniecznie zła rzecz, ale powinieneś zdawać sobie sprawę z tego, o co pytasz.
Doval
17
Zobacz najbardziej funkcjonalne języki
AlexFoxGill
9
@gnat, to zdecydowanie nie jest rantem przeciwko Javie. Programuję prawie wyłącznie w Javie. Chodzi mi o to, że w społeczności Java jest ogólnie akceptowane, że generyczne są wadliwe (nie jest to całkowita awaria, ale prawdopodobnie częściowa), więc logicznym pytaniem jest, jak powinny zostać zaimplementowane. Dlaczego się mylą, a inni poprawili je? A może rzeczywiście nie da się uzyskać generyków absolutnie słusznych?
Peter
1
Czy wszyscy kradliby po C # byłoby mniej skarg. Szczególnie Java jest w stanie nadrobić zaległości poprzez kopiowanie. Zamiast tego decydują się na gorsze rozwiązania. Wiele pytań, które wciąż omawiają komisje projektowe Java, zostało już ustalonych i wdrożonych w języku C #. Nawet nie wyglądają.
usr
2
@emodendroket: Myślę, że moje dwa największe zarzuty dotyczące generycznych C # są takie, że nie ma sposobu na zastosowanie ograniczenia „nadtypu” (np. Foo<T> where SiameseCat:T) i że nie ma możliwości posiadania typu ogólnego, na który nie można zamienić Object. IMHO, .NET skorzystałby na typach agregatów, które były podobne do struktur, ale jeszcze bardziej pozbawione kości. Gdyby KeyValuePair<TKey,TValue>taki był, to IEnumerable<KeyValuePair<SiameseCat,FordFocus>>można by rzucić na IEnumerable<KeyValuePair<Animal,Vehicle>>, ale tylko wtedy, gdy tego typu nie można było spakować.
supercat

Odpowiedzi:

24

Podczas gdy Generics jest głównym nurtem społeczności programistów funkcjonalnych od dziesięcioleci, dodawanie generics do obiektowych języków programowania oferuje pewne unikalne wyzwania, w szczególności interakcję podtypów i generics.

Jednak nawet jeśli skupimy się na obiektowych językach programowania, w szczególności na Javie, można zaprojektować znacznie lepszy system generyczny:

  1. Typy ogólne powinny być dopuszczalne wszędzie tam, gdzie są inne typy. W szczególności, jeśli Tjest parametrem typu, następujące wyrażenia powinny zostać skompilowane bez ostrzeżeń:

    object instanceof T; 
    T t = (T) object;
    T[] array = new T[1];
    

    Tak, wymaga to weryfikacji ogólnych, tak jak każdego innego typu w języku.

  2. Kowariancja i kontrawariancja typu ogólnego powinny być określone w jej deklaracji (lub wywnioskowane z), a nie za każdym razem, gdy używany jest typ ogólny, abyśmy mogli napisać

    Future<Provider<Integer>> s;
    Future<Provider<Number>> o = s; 
    

    zamiast

    Future<? extends Provider<Integer>> s;
    Future<? extends Provider<? extends Number>> o = s;
    
  3. Ponieważ typy ogólne mogą być dość długie, nie powinniśmy musieć ich określać nadmiarowo. To znaczy, powinniśmy móc pisać

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (var e : map.values()) {
        for (var list : e.values()) {
            for (var person : list) {
                greet(person);
            }
        }
    }
    

    zamiast

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (Map<String, List<LanguageDesigner>> e : map.values()) {
        for (List<LanguageDesigner> list : e.values()) {
            for (LanguageDesigner person : list) {
                greet(person);
            }
        }
    }
    
  4. Każdy typ powinien być dopuszczalny jako parametr typu, a nie tylko typy referencyjne. (Jeśli możemy mieć int[], dlaczego nie możemy mieć List<int>)?

Wszystko to jest możliwe w C #.

meriton - podczas strajku
źródło
1
Czy pozbyłoby się to również generycznych autoreferencji? Co jeśli chcę powiedzieć, że porównywalny obiekt może się porównywać z czymkolwiek tego samego typu lub podklasy? Czy da się to zrobić? Lub jeśli napiszę metodę sortowania, która akceptuje listy z porównywalnymi obiektami, wszystkie one muszą być do siebie porównywalne. Enum jest kolejnym dobrym przykładem: Enum <E rozszerza Enum <E>>. Nie twierdzę, że system typów powinien być w stanie to zrobić, jestem tylko ciekawy, jak C # radzi sobie z tymi sytuacjami.
Peter
1
Wnioskowanie o typach języka Java 7 i automatyczna pomoc C ++ w niektórych z tych problemów, ale są one składniowe i nie zmieniają podstawowych mechanizmów.
Wnioskowanie o typie języka Java w usłudze Snowman ma kilka naprawdę nieznośnych przypadków narożnych, takich jak brak pracy z anonimowymi klasami i nie znalezienie właściwych granic dla symboli wieloznacznych, gdy oceniasz metodę ogólną jako argument do innej metody ogólnej.
Doval,
@Doval dlatego powiedziałem, że pomaga w rozwiązaniu niektórych problemów: nic nie naprawia i nie rozwiązuje wszystkiego. Generyczne programy Java mają wiele problemów: chociaż lepsze niż typy surowe, z pewnością powodują wiele problemów.
34

Korzystanie z podtypów powoduje wiele komplikacji podczas programowania ogólnego. Jeśli nalegasz na używanie języka z podtypami, musisz zaakceptować, że wraz z nim towarzyszy pewna złożoność programowania ogólnego. Niektóre języki robią to lepiej niż inne, ale możesz to zrobić tylko do tej pory.

Porównaj to na przykład z lekami generycznymi Haskella. Są one na tyle proste, że jeśli używasz typu wnioskowania, można napisać poprawną funkcję rodzajowe przez przypadek . W rzeczywistości, jeśli podasz jeden typ, kompilator często mówi do siebie: „No, I był zamiar zrobić to uniwersalne, ale poprosił mnie, aby go tylko za wskazówki, więc cokolwiek.”

Trzeba przyznać, że ludzie używają systemu czcionek Haskell w zaskakująco skomplikowany sposób, co sprawia, że ​​jest zmorą każdego początkującego, ale sam system typów jest elegancki i bardzo podziwiany.

Karl Bielefeldt
źródło
1
Dziękuję za tę odpowiedź. Ten artykuł zaczyna się od kilku przykładów Joshuy Blocha, w których generycy stają się zbyt skomplikowani: artima.com/weblogs/viewpost.jsp?thread=222021 . Czy jest to różnica w kulturze między Javą a Haskellem, gdzie takie konstrukty byłyby w Haskell uważane za dobre, czy też istnieje prawdziwa różnica w systemie typów Haskella, który unika takich sytuacji?
Peter
10
@Peter Haskell nie ma podtytułu i, jak powiedział Karl, kompilator może automatycznie wnioskować o typach, włączając w to ograniczenia typu „typ amusi być jakąś liczbą całkowitą”.
Doval
Innymi słowy, kowariancja , w językach takich jak Scala.
Paul Draper,
14

Około 20 lat temu przeprowadzono sporo badań nad połączeniem leków generycznych z subtyfikacją. Język programowania Thor opracowany przez grupę badawczą Barbary Liskov z MIT miał pojęcie klauzul „gdzie”, które pozwalają określić wymagania dotyczące rodzaju, który parametryzujesz. (Jest to podobne do tego, co C ++ próbuje zrobić z Concepts .)

Artykuł opisujący leki generyczne Thora i ich interakcje z podtypami Thora to: Dzień, M; Gruber, R; Liskov, B; Myers, AC: Podtypy vs. klauzule where: ograniczanie polimorfizmu parametrycznego , ACM Conf na Obj-Oriented Prog, Sys, Lang i Apps , (OOPSLA-10): 156-158, 1995.

Wierzę, że oni z kolei oparli się na pracach wykonanych na Emerald pod koniec lat osiemdziesiątych. (Nie czytałem tej pracy, ale referencja to: Black, A; Hutchinson, N; Jul, E; Levy, H; Carter, L: Distribution and Abstract Types in Emerald , _IEEE T. Software Eng., 13 ( 1): 65–76, 1987.

Zarówno Thor, jak i Emerald byli „językami akademickimi”, więc prawdopodobnie nie mieli wystarczającego wykorzystania, aby ludzie naprawdę mogli zrozumieć, czy gdzie klauzule (pojęcia) naprawdę rozwiązują jakiekolwiek rzeczywiste problemy. Interesujące jest przeczytanie artykułu Bjarne Stroustrup o tym, dlaczego pierwsza próba Koncepcji w C ++ nie powiodła się: Stroustrup, B: Decyzja C ++ 0x „Usuń koncepcje” , Dr Dobbs , 22 lipca 2009 r. (Więcej informacji na stronie głównej Stroustrup . )

Kolejnym kierunkiem, który ludzie wydają się próbować, jest coś, co nazywa się cechami . Na przykład język programowania Rust w Mozilli wykorzystuje cechy. Jak rozumiem (co może być całkowicie błędne), deklarowanie, że klasa spełnia pewną cechę, jest bardzo podobne do powiedzenia, że ​​klasa implementuje interfejs, ale mówisz „zachowuje się jak„ raczej niż „jest”. Wydaje się, że nowe języki programowania Swift firmy Apple wykorzystują podobną koncepcję protokołów w celu określenia ograniczeń parametrów w stosunku do ogólnych .

Wędrująca logika
źródło