Jaki jest kompromis dla wnioskowania o typie?

29

Wygląda na to, że wszystkie nowe języki programowania lub przynajmniej te, które stały się popularne, korzystają z wnioskowania o typach. Nawet Javascript dostał typy i wnioskowanie o typach przez różne implementacje (Acscript, maszynopis itp.). Wygląda mi to świetnie, ale zastanawiam się, czy są jakieś kompromisy lub dlaczego powiedzmy, że Java lub stare dobre języki nie mają wnioskowania typu

  • Deklarując zmienną w Go bez określania jej typu (używając var bez typu lub składni: =), typ zmiennej jest wywnioskowany z wartości po prawej stronie.
  • D umożliwia pisanie dużych fragmentów kodu bez zbędnego określania typów, tak jak robią to języki dynamiczne. Z drugiej strony, wnioskowanie statyczne dedukuje typy i inne właściwości kodu, dając to, co najlepsze zarówno ze świata statycznego, jak i dynamicznego.
  • Silnik wnioskowania typu w Rust jest dość inteligentny. Robi więcej niż patrzenie na typ wartości r podczas inicjalizacji. Wygląda również, jak zmienna jest później używana do wnioskowania o jej typie.
  • Swift korzysta z wnioskowania o typie, aby opracować odpowiedni typ. Wnioskowanie typu umożliwia kompilatorowi automatyczne wydedukowanie typu określonego wyrażenia podczas kompilowania kodu, po prostu poprzez sprawdzenie podanych wartości.
Użytkownik bez kapelusza
źródło
3
W języku C # ogólne wytyczne mówią, że nie zawsze należy używać, varponieważ może to czasem zaszkodzić czytelności.
Mephy
5
„… lub dlaczego powiedzmy, że Java lub stare dobre języki nie mają wnioskowania na podstawie typu” Przyczyny są prawdopodobnie historyczne; ML pojawił się 1 rok po C według Wikipedii i miał wnioskowanie o typach. Java próbowała odwołać się do programistów C ++ . C ++ zaczęło się jako rozszerzenie C, a głównymi obawami C było przenośne opakowanie nad zestawem i łatwość kompilacji. Niezależnie od tego, co jest warte, przeczytałem, że podtypowanie powoduje, że wnioskowanie o typach jest również nierozstrzygalne w ogólnym przypadku.
Doval
3
@Doval Scala wydaje się robić dobrą robotę w zakresie wnioskowania o typach, dla języka, który obsługuje dziedziczenie podtypów. Nie jest tak dobry, jak którykolwiek z języków rodziny ML, ale prawdopodobnie jest tak dobry, jak można by poprosić, biorąc pod uwagę projekt języka.
KChaloux
12
Warto rozróżnić odliczenie typu (jednokierunkowe, jak C # vari C ++ auto) i wnioskowanie o typie (dwukierunkowe, jak Haskell let). W pierwszym przypadku typ nazwy można wywnioskować tylko z jego inicjatora - jego użycie musi być zgodne z typem nazwy. W tym drugim przypadku typ nazwy można również wywnioskować z jego użycia - co jest użyteczne, ponieważ można pisać po prostu []dla pustej sekwencji, niezależnie od typu elementu, lub newEmptyMVardla nowego, zmiennego odwołania, niezależnie od odnośnika rodzaj.
Jon Purdy
Wnioskowanie typu może być niezwykle skomplikowane w implementacji, szczególnie efektywnie. Ludzie nie chcą, aby ich złożoność kompilacji ulegała wykładniczemu wybuchowi z powodu bardziej złożonych wyrażeń. Ciekawe przeczytanie: cocoawithlove.com/blog/2016/07/12/type-checker-issues.html
Alexander - Przywróć Monikę

Odpowiedzi:

43

System typów Haskella jest w pełni nie do zniesienia (pomijając rekurencję polimorficzną, pewne rozszerzenia językowe i przerażające ograniczenie monomorfizmu ), ale programiści wciąż często dostarczają adnotacje typu w kodzie źródłowym, nawet jeśli nie muszą. Czemu?

  1. Adnotacje typu służą jako dokumentacja . Jest to szczególnie prawdziwe w przypadku typów tak ekspresyjnych jak Haskell. Biorąc pod uwagę nazwę funkcji i jej typ, zazwyczaj można dość dobrze zgadywać, co robi funkcja, szczególnie gdy funkcja jest parametrycznie polimorficzna.
  2. Wpisz adnotacje, które mogą przyspieszyć rozwój . Pisanie podpisu typu przed napisaniem treści funkcji wydaje się czymś w rodzaju programowania opartego na testach. Z mojego doświadczenia wynika, że ​​po skompilowaniu funkcji Haskell zwykle działa ona za pierwszym razem. (Oczywiście nie eliminuje to potrzeby zautomatyzowanych testów!)
  3. Jawne typy mogą pomóc w sprawdzeniu założeń . Kiedy próbuję zrozumieć kod, który już działa, często przesypuję go adnotacjami poprawnego typu. Jeśli kod nadal się kompiluje, wiem, że go zrozumiałem. Jeśli nie, przeczytałem komunikat o błędzie.
  4. Podpisy typów pozwalają wyspecjalizować funkcje polimorficzne . Bardzo rzadko interfejs API jest bardziej wyrazisty lub przydatny, jeśli niektóre funkcje nie są polimorficzne. Kompilator nie będzie narzekał, jeśli dasz funkcji mniej ogólny typ niż można by wywnioskować. Klasycznym przykładem jest map :: (a -> b) -> [a] -> [b]. Jego bardziej ogólna forma ( fmap :: Functor f => (a -> b) -> f a -> f b) dotyczy wszystkich Functors, nie tylko list. Uznano jednak, że mapdla początkujących będzie to łatwiejsze do zrozumienia, dlatego żyje wraz ze swoim większym bratem.

Ogólnie rzecz biorąc, wady systemu o typie statycznym, ale nie do zniesienia są w dużej mierze takie same, jak wady pisania statycznego w ogóle, zużyta dyskusja na tej stronie i innych (Googling „wady statycznego pisania” przyniesie setki stron wojen ogniowych). Oczywiście, niektóre z tych wad zostały złagodzone przez mniejszą liczbę adnotacji typu w systemie niższym. Co więcej, wnioskowanie na temat typu ma swoje zalety: rozwój dziurawy nie byłby możliwy bez wnioskowania na temat typu.

Java * dowodzi, że język wymagający zbyt wielu adnotacji typu staje się denerwujący, ale przy zbyt małej liczbie tracisz zalety, które opisałem powyżej. Języki, w których wnioskuje się o rezygnacji, zapewniają odpowiednią równowagę między dwoma skrajnościami.

* Nawet Java, ten wielki kozioł ofiarny, dokonuje pewnej liczby wnioskowania typu lokalnego . W takim stwierdzeniu Map<String, Integer> = new HashMap<>();nie musisz określać ogólnego typu konstruktora. Z drugiej strony języki w stylu ML są zazwyczaj globalnie gorsze.

Benjamin Hodgson
źródło
2
Ale nie jesteś początkującym;) Musisz naprawdę zrozumieć klasy i rodzaje, zanim naprawdę „dostaniesz” Functor. Wymienia i mapczęściej znają niesezonowanych Haskellerów.
Benjamin Hodgson,
1
Czy uważasz C # varza przykład wnioskowania typu?
Benjamin Hodgson,
1
Tak, C # varjest właściwym przykładem.
Doval
1
Witryna już oznaczyła tę dyskusję jako zbyt długą, więc będzie to moja ostatnia odpowiedź. W tym pierwszym kompilator musi określić typ x; w tym ostatnim nie ma typu do ustalenia, wszystkie typy są znane i wystarczy sprawdzić, czy wyrażenie ma sens. Różnica staje się ważniejsza, gdy przechodzisz obok trywialnych przykładów i xjest używana w wielu miejscach; kompilator musi następnie sprawdzić krzyżowo lokalizacje, w których xsłuży do określenia 1) czy można przypisać xtyp, aby kod sprawdził typ? 2) Jeśli tak, jaki najbardziej ogólny typ możemy mu przypisać?
Doval
1
Zauważ, że new HashMap<>();składnia została dodana tylko w Javie 7, a lambda w Javie 8 pozwalają na całkiem sporo wnioskowania typu „rzeczywistego”.
Michael Borgwardt,
8

W języku C # wnioskowanie typu występuje w czasie kompilacji, więc koszt środowiska wykonawczego wynosi zero.

Ze względu na styl varjest stosowany w sytuacjach, w których ręczne określenie typu jest niewygodne lub niepotrzebne. Linq jest jedną z takich sytuacji. Innym jest:

var s = new SomeReallyLongTypeNameWith<Several, Type, Parameters>(andFormal, parameters);

bez których powtarzałbyś swoją naprawdę długą nazwę typu (i parametry typu) zamiast po prostu mówić var.

Użyj jawnej nazwy typu, gdy jest jawny, poprawia przejrzystość kodu.

Istnieją sytuacje, w których nie można użyć wnioskowania o typie, takie jak deklaracje zmiennych składowych, których wartości są ustawione w czasie budowy, lub gdy naprawdę chcesz, aby intellisense działało poprawnie (IDE Hackerrank nie zilustruje członków zmiennej, chyba że wyraźnie zadeklarujesz typ) .

Robert Harvey
źródło
16
„W języku C # wnioskowanie typu występuje w czasie kompilacji, więc koszt środowiska wykonawczego wynosi zero”. Wnioskowanie typu występuje z definicji w czasie kompilacji, więc tak jest w każdym języku.
Doval
Tym lepiej.
Robert Harvey
1
@Doval można wnioskować o typie podczas kompilacji JIT, co oczywiście ma koszt w czasie wykonywania.
Jules
6
@Jules Jeśli dotarłeś tak daleko do uruchomienia programu, to jest on już sprawdzony pod względem typu; nie ma już nic do wnioskowania. To, o czym mówisz, nie jest zwykle nazywane wnioskowaniem typu, chociaż nie jestem pewien, jaki jest właściwy termin.
Doval
7

Dobre pytanie!

  1. Ponieważ ten typ nie jest wyraźnie opatrzony adnotacjami, może czasami utrudniać odczytanie kodu - prowadząc do większej liczby błędów. Właściwie użyte to oczywiście czyni kod czystszym i bardziej czytelnym. Jeśli jesteś pesymistą i uważasz, że większość programistów jest zła (lub pracuje tam, gdzie większość programistów jest zła), będzie to strata netto.
  2. Chociaż algorytm wnioskowania typu jest stosunkowo prosty, nie jest bezpłatny . Ten rodzaj rzeczy nieznacznie wydłuża czas kompilacji.
  3. Ponieważ typ nie jest wyraźnie opisany, twoje IDE nie może zgadnąć, co próbujesz zrobić, szkodząc autouzupełnianiu i podobnym pomocnikom podczas procesu deklaracji.
  4. W połączeniu z przeciążeniem funkcji czasami można dostać się do sytuacji, w których algorytm wnioskowania typu nie może zdecydować, którą ścieżkę wybrać, co prowadzi do brzydszych adnotacji w stylu rzutowania. (Zdarza się to dość na przykład w przypadku anonimowej składni funkcji C #).

I jest więcej ezoterycznych języków, które nie mogą zrobić swojej dziwności bez wyraźnej adnotacji typu. Jak dotąd nie znam żadnych, które byłyby na tyle powszechne / popularne / obiecujące, że można by o nich wspomnieć, z wyjątkiem przelotu.

Telastyn
źródło
1
autouzupełnianie nie przejmuje się wnioskowaniem typu. Nie ma znaczenia, w jaki sposób wybrano typ, tylko to, co ma typ. Problemy z lambdami języka C # nie mają nic wspólnego z wnioskami, są one spowodowane tym, że lambdy są polimorficzne, ale system typów nie może sobie z tym poradzić - co nie ma nic wspólnego z wnioskowaniem.
DeadMG,
2
@deadmg - jasne, po zadeklarowaniu zmiennej nie ma problemu, ale podczas pisania deklaracji zmiennej nie może zawęzić opcji prawej strony do zadeklarowanego typu, ponieważ nie ma zadeklarowanego typu.
Telastyn
@deadmg - jeśli chodzi o lambdas, nie jestem do końca pewien, co masz na myśli, ale jestem pewien, że nie jest to całkowicie poprawne w oparciu o moje zrozumienie tego, jak rzeczy działają. Nawet coś tak prostego, jak var foo = x => x;zawodzi, ponieważ język musi się xtutaj wywnioskować i nie ma już nic do roboty. Kiedy budowane są lambdy, są one budowane tak, jak delegaci o wyraźnym typie Func<int, int> foo = x => xsą wbudowani w CIL tak, jak Func<int, int> foo = new Func<int, int>(x=>x);lambda jest generowana jako wygenerowana, wyraźnie wpisana funkcja. Dla mnie wnioskowanie o rodzaju xjest
nieodłączną
3
@Telastyn To nieprawda, że ​​język nie ma nic do roboty. Wszystkie informacje potrzebne do wpisania wyrażenia x => xzawarte są w samej lambdzie - nie odnoszą się do żadnych zmiennych z otaczającego zakresu. Każdy rozsądny język funkcjonalny będzie poprawnie wywnioskować, że jego typ to „dla wszystkich typów a, a -> a”. Myślę, że DeadMG próbuje powiedzieć, że system typów C # nie ma głównej właściwości type, co oznacza, że ​​zawsze można znaleźć najbardziej ogólny typ dla dowolnego wyrażenia. Jest to bardzo łatwa do zniszczenia właściwość.
Doval,
@Doval - enh ... nie ma czegoś takiego jak ogólne obiekty funkcji w C #, które moim zdaniem są nieco inne niż te, o których mówicie, nawet jeśli prowadzi to do takich samych problemów.
Telastyn
4

Wygląda mi to świetnie, ale zastanawiam się, czy są jakieś kompromisy lub dlaczego powiedzmy, że Java lub stare dobre języki nie mają wnioskowania typu

Java jest tutaj wyjątkiem, a nie regułą. Nawet C ++ (który moim zdaniem kwalifikuje się jako „dobry stary język” :)) obsługuje wnioskowanie typu ze autosłowem kluczowym od standardu C ++ 11. Działa nie tylko z deklaracją zmiennej, ale także jako typ zwracanej funkcji, co jest szczególnie przydatne w przypadku niektórych złożonych funkcji szablonu.

Utajone pisanie i wnioskowanie o typie ma wiele dobrych przypadków użycia, a są też przypadki użycia, w których naprawdę nie powinieneś tego robić. Czasami jest to kwestia gustu, a także temat debaty.

Ale to, że istnieją niewątpliwie dobre przypadki użycia, jest samo w sobie uzasadnionym powodem dla każdego języka, aby go wdrożyć. Nie jest to również trudne do zaimplementowania, nie ma ujemnego wpływu na środowisko uruchomieniowe i nie wpływa znacząco na czas kompilacji.

Nie widzę prawdziwej wady polegającej na tym, że daje programistom możliwość wnioskowania o typie.

Niektórzy odpowiadający zastanawiają się, jak dobre jest pisanie na klawiaturze, i na pewno mają rację. Ale brak obsługi niejawnego pisania oznaczałoby, że język wymusza ciągłe pisanie przez cały czas.

Tak więc prawdziwą wadą jest to, że język nie obsługuje niejawnego pisania, ponieważ oznacza to, że nie ma uzasadnionego powodu, aby użyć go, co nie jest prawdą.

Gábor Angyal
źródło
1

Podstawową różnicą między systemem wnioskowania typu Hindleya-Milnera i wnioskowania typu Go jest kierunek przepływu informacji. W HM typ informacji przepływa do przodu i do tyłu poprzez unifikację; w Go wpisz informacje przepływa tylko do przodu: oblicza tylko substytucje do przodu.

Wnioskowanie o typach HM jest wspaniałą innowacją, która dobrze działa z językami funkcjonalnymi o typie polimorficznym, ale autorzy Go prawdopodobnie twierdzą, że próbuje zrobić zbyt wiele:

  1. Fakt, że informacja przepływa zarówno do przodu, jak i do tyłu oznacza, że ​​wnioskowanie o typie HM jest bardzo nielokalną sprawą; przy braku adnotacji typu, każdy wiersz kodu w twoim programie może przyczyniać się do pisania pojedynczego wiersza kodu. Jeśli masz tylko podstawianie do przodu, wiesz, że źródłem błędu typu musi być kod poprzedzający błąd; nie kod, który pojawia się po.

  2. Dzięki wnioskowaniu typu HM masz tendencję do myślenia w ograniczeniach: kiedy używasz typu, ograniczasz jego możliwe typy. Na koniec dnia mogą istnieć pewne zmienne typu, które są całkowicie nieograniczone. Wnioskowanie o typach HM polega na tym, że te typy naprawdę nie mają znaczenia, dlatego są one przekształcane w zmienne polimorficzne. Jednak ten dodatkowy polimorfizm może być niepożądany z wielu powodów. Po pierwsze, jak zauważyli niektórzy, ten dodatkowy polimorfizm może być niepożądany: HM zawiera fałszywy, polimorficzny typ jakiegoś fałszywego kodu, a później prowadzi do dziwnych błędów. Po drugie, gdy typ zostanie pozostawiony polimorficzny, może to mieć konsekwencje dla zachowania środowiska wykonawczego. Na przykład zbyt polimorficzny wynik pośredni jest powodem, dla którego „pokaż. read jest uważany za niejednoznaczny w Haskell; jako inny przykład

Edward Z. Yang
źródło
2
Niestety wygląda to na dobrą odpowiedź na inne pytanie. OP pyta o „wnioskowanie o stylu typu Go” w porównaniu z językami, które w ogóle nie mają wnioskowania o typie, a nie o „styl typu Go” w porównaniu z HM.
Ixrec
-2

Utrudnia to czytelność.

Porównać

var stuff = someComplexFunction()

vs

List<BusinessObject> stuff = someComplexFunction()

Jest to szczególnie problem podczas czytania poza IDE, jak na github.

miguel
źródło
4
wydaje się, że nie oferuje to nic istotnego w porównaniu z punktami przedstawionymi i wyjaśnionymi w poprzednich 6 odpowiedziach
gnat
Jeśli użyjesz prawidłowej nazwy zamiast „złożonego nieczytelnego”, varmożesz z niej korzystać bez uszczerbku dla czytelności. var objects = GetBusinessObjects();
Fabio
Ok, ale przeciekasz system typów do nazw zmiennych. To jak węgierski zapis. Po refaktoryzacji częściej trafisz na nazwy, które nie pasują do kodu. Ogólnie rzecz biorąc, funkcjonalny styl popycha Cię do utraty naturalnych zalet przestrzeni nazw zapewnianych przez klasy. W rezultacie otrzymujesz więcej squareArea () zamiast square.area ().
Miguel