Dlaczego wnioskowanie typu jest przydatne?

37

Czytam kod znacznie częściej niż piszę kod i zakładam, że większość programistów pracujących nad oprogramowaniem przemysłowym to robi. Zakładam, że zaletą wnioskowania typu jest mniejsza szczegółowość i mniej napisany kod. Ale z drugiej strony, jeśli czytasz kod częściej, prawdopodobnie będziesz chciał kodu czytelnego.

Kompilator określa typ; istnieją na to stare algorytmy. Ale prawdziwe pytanie brzmi: dlaczego ja, programista, chciałbym wnioskować o rodzaju moich zmiennych podczas czytania kodu? Czy to nie jest szybsze, aby ktokolwiek po prostu czytał ten typ, niż zastanawiał się, jaki on jest?

Edycja: Podsumowując, rozumiem, dlaczego jest przydatny. Ale w kategorii funkcji językowych widzę to w wiadrze z przeciążeniem operatora - przydatne w niektórych przypadkach, ale wpływające na czytelność, jeśli są nadużywane.

m3th0dman
źródło
5
Z mojego doświadczenia wynika, że pisanie kodu jest znacznie ważniejsze niż czytanie. Podczas czytania kodu szukam algorytmów i konkretnych bloków, do których dobrze nazwane zmienne zwykle będą mnie kierować. Naprawdę nie muszę pisać kodu kontrolnego, aby przeczytać i zrozumieć, co robi, chyba że jest strasznie źle napisany. Jednak podczas czytania kodu, który jest wypełniony dodatkowymi niepotrzebnymi szczegółami, których nie szukam (jak zbyt wiele adnotacji typu), często trudniej jest znaleźć szukane bity. Wnioskowanie typu powiedziałbym, że jest wielkim dobrodziejstwem dla czytania o wiele więcej niż pisania kodu.
Jimmy Hoffa
Gdy znajdę fragment kodu, którego szukam, mógłbym zacząć sprawdzać pisanie, ale w danym momencie nie powinieneś skupiać się na więcej niż 10 liniach kodu, w którym to momencie nie ma problemu z wykonaniem wywnioskuj siebie, ponieważ mentalnie wybierasz cały blok na początku i prawdopodobnie używasz narzędzi, aby ci to pomóc. Ustalenie rodzajów 10 linii kodu, na które próbujesz wprowadzić strefę, rzadko zajmuje dużo czasu, ale jest to część, w której i tak przeszedłeś z czytania na pisanie, co i tak jest znacznie mniej powszechne.
Jimmy Hoffa
Pamiętaj, że chociaż programista czyta kod częściej niż go pisze, nie oznacza to, że fragment kodu jest czytany częściej niż zapisywany. Duża część kodu może być krótkotrwała lub w inny sposób nigdy więcej nie czytana, i może nie być łatwo stwierdzić, który kod przetrwa i należy go zapisać do maksymalnej czytelności.
jpa
2
Rozważając pierwszy punkt @ JimmyHoffa, rozważ ogólnie czytanie. Czy zdanie jest łatwiejsze do przeanalizowania i przeczytania, a co dopiero zrozumienia, skupiając się na części mowy poszczególnych słów? „Krowa (rzeczownik) (rzeczownik-liczba pojedyncza) przeskoczyła (czasownik-przeszłość) nad (przyimek) księżyc (rzeczownik) (rzeczownik). (Okres interpunkcyjny)”.
Zev Spitz,

Odpowiedzi:

46

Rzućmy okiem na Javę. Java nie może mieć zmiennych z wnioskowanymi typami. Oznacza to, że często muszę przeliterować typ, nawet jeśli dla czytelnika jest zupełnie oczywiste, jaki jest typ:

int x = 42;  // yes I see it's an int, because it's a bloody integer literal!

// Why the hell do I have to spell the name twice?
SomeObjectFactory<OtherObject> obj = new SomeObjectFactory<>();

A czasem pisanie całego typu jest po prostu denerwujące.

// this code walks through all entries in an "(int, int) -> SomeObject" table
// represented as two nested maps
// Why are there more types than actual code?
for (Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>> row : table.entrySet()) {
    Integer rowKey = entry.getKey();
    Map<Integer, SomeObject<SomeObject, T>> rowValue = entry.getValue();
    for (Map.Entry<Integer, SomeObject<SomeObject, T>> col : rowValue.entrySet()) {
        Integer colKey = col.getKey();
        SomeObject<SomeObject, T> colValue = col.getValue();
        doSomethingWith<SomeObject<SomeObject, T>>(rowKey, colKey, colValue);
    }
}

Pełne pisanie statyczne przeszkadza mi, programistom. Większość adnotacji typu to powtarzające się wypełnianie wierszy, pozbawione treści zwroty tego, co już wiemy. Lubię jednak pisanie statyczne, ponieważ może to naprawdę pomóc w wykrywaniu błędów, więc używanie pisania dynamicznego nie zawsze jest dobrą odpowiedzią. Wnioskowanie o typach jest najlepsze z obu światów: mogę pominąć niepotrzebne typy, ale nadal mam pewność, że mój program (typ) się sprawdzi.

Chociaż wnioskowanie typu jest naprawdę przydatne w przypadku zmiennych lokalnych, nie powinno się go stosować w publicznych interfejsach API, które muszą być jednoznacznie udokumentowane. A czasem typy naprawdę mają kluczowe znaczenie dla zrozumienia, co dzieje się w kodzie. W takich przypadkach głupotą byłoby polegać wyłącznie na wnioskowaniu typu.

Istnieje wiele języków, które obsługują wnioskowanie typu. Na przykład:

  • C ++. Te autowyzwalacze słów kluczowych wpisać wnioskowanie. Bez niego wypisywanie rodzajów lambdów lub wpisów w pojemnikach byłoby piekłem.

  • DO#. Można zadeklarować zmienne var, co wyzwala ograniczoną formę wnioskowania o typie. Nadal zarządza większością przypadków, w których chcesz wnioskować o typie. W niektórych miejscach możesz całkowicie pominąć ten typ (np. W lambda).

  • Haskell i każdy język w rodzinie ML. Chociaż zastosowany tutaj specyficzny sposób wnioskowania o typie jest dość potężny, nadal często widzisz adnotacje o typach funkcji z dwóch powodów: Pierwszy to dokumentacja, a drugi to sprawdzenie, czy wnioskowanie o typach rzeczywiście znalazło oczekiwane typy. Jeśli występuje rozbieżność, prawdopodobnie występuje jakiś błąd.

amon
źródło
13
Zauważ też, że C # ma typy anonimowe, tj. Typy bez nazwy, ale C # ma nominalny system typów, tj. System typów oparty na nazwach. Bez wnioskowania o typach nigdy nie można użyć tych typów!
Jörg W Mittag
10
Moim zdaniem niektóre przykłady są nieco wymyślone. Inicjalizacja do 42 nie oznacza automatycznie, że zmienna jest int, może to być dowolny typ liczbowy, w tym parzysty char. Nie rozumiem też, dlaczego chcesz przeliterować cały typ, Entrykiedy możesz po prostu wpisać nazwę klasy i pozwolić twojemu IDE wykonać niezbędny import. Jedynym przypadkiem, gdy musisz przeliterować całe imię, jest posiadanie klasy o tej samej nazwie we własnym pakiecie. Ale i tak wydaje mi się, że to zły projekt.
Malcolm
10
@Malcolm Tak, wszystkie moje przykłady zostały opracowane. Służą one do zilustrowania punktu. Kiedy pisałem intprzykład, myślałem o (moim zdaniem całkiem rozsądnym zachowaniu) większości języków, w których wnioskuje się o typie. Zwykle wywnioskować intlub Integerczy cokolwiek to nazywa się w tym języku. Piękno wnioskowania typu polega na tym, że jest ono zawsze opcjonalne; nadal możesz określić inny typ, jeśli go potrzebujesz. Jeśli chodzi o Entryprzykład: dobra uwaga, zastąpię go Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>. Java nie ma nawet aliasów typów :(
amon
4
@ m3th0dman Jeśli typ jest ważny dla zrozumienia, możesz nadal wyraźnie o nim wspomnieć. Wnioskowanie typu jest zawsze opcjonalne. Ale tutaj rodzaj colKeyjest zarówno oczywisty, jak i nieistotny: dbamy tylko o to, aby był odpowiedni jako drugi argument doSomethingWith. Gdybym wyodrębnił tę pętlę do funkcji, która daje (key1, key2, value)Iterable of -triples, najbardziej ogólną sygnaturą byłoby <K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table). Wewnątrz tej funkcji prawdziwy typ colKey( Integer, nie K2) jest absolutnie nieistotny.
amon
4
@ m3th0dman to dość szerokie stwierdzenie, że „ większość ” kodu jest tym czy tamtym. Anegdotyczne statystyki. Tam na pewno nie ma sensu pisać typ dwukrotnie w inicjalizatorów: View.OnClickListener listener = new View.OnClickListener(). Nadal znasz typ, nawet jeśli programista byłby „leniwy” i skrócił go do var listener = new View.OnClickListener(jeśli to było możliwe). Ten rodzaj redundancji jest powszechne - nie będę ryzykować guesstimate tutaj - i usunięcie go nie wynika z myślą o przyszłych czytelników. Z każdą funkcją językową należy korzystać ostrożnie, nie kwestionuję tego.
Konrad Morawski
26

Prawdą jest, że kod jest odczytywany znacznie częściej niż jest zapisywany. Jednak czytanie wymaga również czasu, a dwa ekrany kodu są trudniejsze w nawigacji i czytaniu niż jeden ekran kodu, dlatego musimy ustalić priorytet, aby spakować najlepszy użyteczny stosunek informacji / wysiłku odczytu. Jest to ogólna zasada UX: zbyt duża ilość informacji jednocześnie przytłacza i faktycznie obniża efektywność interfejsu.

I z mojego doświadczenia wynika , że często dokładny typ nie jest (to) ważny. Na pewno czasami zagnieżdżać wyrażenia: x + y * z, monkey.eat(bananas.get(i)), factory.makeCar().drive(). Każde z nich zawiera podwyrażenia, których wynikiem jest wartość, której typ nie jest zapisany. Ale są całkowicie jasne. Nie przeszkadza nam pozostawienie tego typu bez opisu, ponieważ łatwo jest go zrozumieć z kontekstu, a jego zapisanie przyniosłoby więcej szkody niż pożytku (zaśmiecenie zrozumienia przepływu danych, zabranie cennego ekranu i krótkotrwałej pamięci).

Jednym z powodów, dla których nie zagnieżdżaj wyrażeń, tak jak nie ma jutra, jest to, że linie stają się długie, a przepływ wartości staje się niejasny. Wprowadzenie zmiennej tymczasowej pomaga w tym, narzuca porządek i nadaje nazwę częściowemu wynikowi. Jednak nie wszystko, co korzysta z tych aspektów, również korzysta z tego, że określono jego typ:

user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)

Czy ma znaczenie, czy obiekt userjest obiektem, liczbą całkowitą, łańcuchem, czy czymś innym? W większości przypadków tak nie jest, wystarczy wiedzieć, że reprezentuje użytkownika, pochodzi z żądania HTTP i służy do pobrania nazwy wyświetlanej w prawym dolnym rogu odpowiedzi.

A gdy ma to znaczenie, autor może napisać ten typ. Jest to swoboda, z której należy korzystać w sposób odpowiedzialny, ale to samo dotyczy wszystkiego, co może poprawić czytelność (nazwy zmiennych i funkcji, formatowanie, projektowanie interfejsu API, białe znaki). I rzeczywiście, konwencja w Haskell i ML (gdzie wszystko można wywnioskować bez dodatkowego wysiłku) polega na wypisaniu typów funkcji funkcji nielokalnych, a także zmiennych lokalnych i funkcji, gdy jest to właściwe. Tylko nowicjusze pozwalają wywnioskować każdy typ.


źródło
2
+1 To powinna być zaakceptowana odpowiedź. Od samego początku rozumowanie, dlaczego wnioskowanie na podstawie typu jest świetnym pomysłem.
Christian Hayter
Dokładny typ userma znaczenie, jeśli próbujesz rozszerzyć funkcję, ponieważ określa ona, co możesz zrobić z user. Jest to ważne, jeśli chcesz dodać kontrolę poprawności (na przykład z powodu luki w zabezpieczeniach) lub zapomniałeś, że musisz coś zrobić z użytkownikiem oprócz samego wyświetlania. To prawda, że ​​tego rodzaju czytanie w celu rozszerzenia występuje rzadziej niż tylko czytanie kodu, ale są również istotną częścią naszej pracy.
cmaster,
@cmaster I zawsze możesz dość łatwo znaleźć ten typ (większość IDE ci to powie, i jest mało zaawansowane rozwiązanie celowego powodowania błędu typu i pozwalania kompilatorowi na wydrukowanie rzeczywistego typu), jest po prostu na uboczu, więc nie drażni cię w zwykłym przypadku.
4

Myślę, że wnioskowanie na temat typu jest dość ważne i powinno być obsługiwane w każdym nowoczesnym języku. Wszyscy rozwijamy się w IDE i mogą one bardzo pomóc na wypadek, gdybyś chciał poznać wyprowadzony typ, tylko kilku z nas włamuje się vi. Pomyśl na przykład o gadatliwości i kodzie ceremonii w Javie.

  Map<String,HashMap<String,String>> map = getMap();

Ale możesz powiedzieć, że to w porządku, moje IDE mi pomoże, to może być ważny punkt. Jednak niektóre funkcje nie byłyby dostępne bez pomocy wnioskowania o typie, na przykład typy anonimowe w języku C #.

 var person = new {Name="John Smith", Age = 105};

LINQ nie byłby tak miły, jak to jest teraz bez pomocy typu wnioskowania, Selectna przykład

  var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});

Ten anonimowy typ zostanie starannie wywnioskowany ze zmiennej.

Nie podoba mi się wnioskowanie o typach zwrotnych, Scalaponieważ myślę, że twój punkt dotyczy tutaj, powinno być dla nas jasne, co zwraca funkcja, abyśmy mogli płynniej korzystać z interfejsu API

Sleiman Jneidi
źródło
Map<String,HashMap<String,String>>? Jasne, jeśli nie używasz typów, pisownia ich ma niewielką korzyść. Table<User, File, String>jest jednak bardziej pouczający, a napisanie go jest korzystne.
MikeFHay
4

Myślę, że odpowiedź na to pytanie jest bardzo prosta: oszczędza czytania i pisania zbędnych informacji. Zwłaszcza w językach zorientowanych obiektowo, w których po obu stronach znaku równości jest czcionka.

Co również informuje, kiedy należy lub nie należy jej używać - gdy informacje nie są zbędne.

jmoreno
źródło
3
Technicznie informacje są zawsze zbędne, gdy można pominąć ręczne podpisy: w przeciwnym razie kompilator nie byłby w stanie ich wywnioskować! Ale rozumiem, co masz na myśli: gdy po prostu duplikujesz podpis do wielu miejsc w jednym widoku, jest on naprawdę zbędny dla mózgu , podczas gdy kilka dobrze umieszczonych typów daje informacje, których będziesz musiał długo szukać, być może z wiele dobrych nieoczywistych transformacji.
leftaroundabout
@leftaroundabout: redundantne podczas odczytu przez programistę.
jmoreno
3

Załóżmy, że ktoś widzi kod:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

Jeśli można someBigLongGenericTypeto przypisać na podstawie typu zwracanego someFactoryMethod, to jak prawdopodobne jest, aby ktoś czytający kod zauważył, jeśli typy nie pasują dokładnie, i jak łatwo ktoś, kto zauważył rozbieżność, mógł rozpoznać, czy było to zamierzone, czy nie?

Umożliwiając wnioskowanie, język może zasugerować komuś, kto czyta kod, że gdy zostanie wyraźnie określony typ zmiennej, osoba ta powinna spróbować znaleźć przyczynę. To z kolei pozwala osobom, które czytają kod, lepiej skoncentrować swoje wysiłki. Jeśli natomiast w zdecydowanej większości przypadków, gdy określony jest typ, zdarza się, że jest dokładnie taki sam, jak to, co można by wywnioskować, to ktoś, kto czyta kod, może być mniej podatny na zauważenie, że jest on nieco inny .

supercat
źródło
2

Widzę, że jest już wiele dobrych odpowiedzi. Niektóre z nich powtórzę, ale czasami po prostu chcesz wyrazić swoje własne słowa. Skomentuję kilka przykładów z C ++, ponieważ jest to język, który znam najbardziej.

To, co jest konieczne, nigdy nie jest nierozsądne. Wnioskowanie typu jest konieczne, aby inne funkcje językowe były praktyczne. W C ++ można mieć typy niewymienne.

struct {
    double x, y;
} p0 = { 0.0, 0.0 };
// there is no name for the type of p0
auto p1 = p0;

C ++ 11 dodał lambdy, które również są niewymienne.

auto sq = [](int x) {
    return x * x;
};
// there is no name for the type of sq

Wnioskowanie typu stanowi również podstawę szablonów.

template <class x_t>
auto sq(x_t const& x)
{
    return x * x;
}
// x_t is not known until it is inferred from an expression
sq(2); // x_t is int
sq(2.0); // x_t is double

Ale twoje pytania brzmiały: „dlaczego ja, programista, chciałbym wnioskować o rodzaju moich zmiennych, gdy czytam kod? Czy nie jest szybsze, aby ktokolwiek po prostu czytał ten typ, niż zastanawiał się, jaki on jest?”.

Wnioskowanie typu usuwa nadmiarowość. Jeśli chodzi o odczyt kodu, czasem może być szybsze i łatwiejsze mieć nadmiarowe informacje w kodzie, ale nadmiarowość może przyćmić przydatne informacje . Na przykład:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

Nie trzeba zbytnio zaznajomić się ze standardową biblioteką dla programisty C ++, aby stwierdzić, że i jest iteratorem, i = v.begin()więc deklaracja typu jawnego ma ograniczoną wartość. Swoją obecnością zasłania szczegóły, które są ważniejsze (takie jak iwskazuje na początek wektora). Dobra odpowiedź @amon stanowi jeszcze lepszy przykład gadatliwości przesłaniającej ważne szczegóły. W przeciwieństwie do tego, wnioskowanie na podstawie typu daje większe znaczenie ważnym szczegółom.

std::vector<int> v;
auto i = v.begin();

Podczas gdy czytanie kodu jest ważne, nie jest wystarczające, w pewnym momencie będziesz musiał przestać czytać i zacząć pisać nowy kod. Nadmiarowość kodu powoduje, że modyfikowanie kodu jest wolniejsze i trudniejsze. Powiedzmy, że mam następujący fragment kodu:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

W przypadku, gdy muszę zmienić typ wartości wektora, aby dwukrotnie zmienić kod na:

std::vector<double> v;
std::vector<double>::iterator i = v.begin();

W takim przypadku muszę zmodyfikować kod w dwóch miejscach. Porównaj z wnioskowaniem typu, gdy oryginalny kod to:

std::vector<int> v;
auto i = v.begin();

I zmodyfikowany kod:

std::vector<double> v;
auto i = v.begin();

Zauważ, że teraz muszę zmienić tylko jeden wiersz kodu. Ekstrapoluj to na duży program, a wnioskowanie o typach może propagować zmiany do typów znacznie szybciej niż w edytorze.

Nadmiarowość w kodzie stwarza możliwość błędów. Za każdym razem, gdy kod zależy od zachowania równoważności dwóch informacji, istnieje możliwość pomyłki. Na przykład występuje niespójność między tymi dwoma typami w tej instrukcji, co prawdopodobnie nie jest zamierzone:

int pi = 3.14159;

Redundancja utrudnia rozpoznanie intencji. W niektórych przypadkach wnioskowanie typu może być łatwiejsze do odczytania i zrozumienia, ponieważ jest prostsze niż jawna specyfikacja typu. Rozważ fragment kodu:

int y = sq(x);

W przypadku, gdy sq(x)zwraca an int, nie jest oczywiste, czy yjest to, intponieważ jest to typ zwracany, sq(x)czy dlatego, że pasuje do używanych instrukcji y. Jeśli zmienię inny kod tak, aby sq(x)nie powrócił int, z samej linii nie jest pewne, czy typ ypowinien zostać zaktualizowany. Porównaj z tym samym kodem, ale używając wnioskowania typu:

auto y = sq(x);

W tym celu intencja jest jasna, ymusi być tego samego typu, co zwrócony przez sq(x). Gdy kod zmienia typ zwracany sq(x), typ yzmian jest dopasowywany automatycznie.

W C ++ istnieje drugi powód, dla którego powyższy przykład jest prostszy w przypadku wnioskowania typu, wnioskowanie typu nie może wprowadzać niejawnej konwersji typu. Jeśli zwracany typ sq(x)nie jest int, kompilator po cichu wstawia niejawną konwersję na int. Jeśli typ zwracany sq(x)jest typem złożonym, który definiuje operator int(), to ukryte wywołanie funkcji może być dowolnie złożone.

Bowie Owens
źródło
Całkiem dobra uwaga na temat niewypowiedzianych typów w C ++. Sądzę jednak, że nie jest to wystarczający powód do dodawania wnioskowania o typie, niż jest to powód do poprawienia języka. W pierwszym przypadku, który przedstawisz, programista musiałby po prostu nadać temu rzeczowi nazwę, aby uniknąć używania wnioskowania typu, więc nie jest to mocny przykład. Drugi przykład jest tylko mocny, ponieważ C ++ wyraźnie zabrania wypowiadania typów lambda, nawet używanie czcionek przy użyciu typeofjest bezużyteczne dla języka. I to jest moim zdaniem deficyt samego języka.
cmaster