Qual è il compromesso per l'inferenza del tipo?


29

Sembra che tutti i nuovi linguaggi di programmazione o almeno quelli che sono diventati popolari usino l'inferenza del tipo. Anche Javascript ha ottenuto tipi e inferenze di tipo attraverso varie implementazioni (Acscript, dattiloscritto ecc.). Mi sembra fantastico, ma mi chiedo se ci siano dei compromessi o perché diciamo che Java o le vecchie buone lingue non hanno inferenza di tipo

  • Quando si dichiara una variabile in Go senza specificarne il tipo (usando var senza un tipo o la sintassi: =), il tipo della variabile viene dedotto dal valore sul lato destro.
  • D consente di scrivere frammenti di codice di grandi dimensioni senza specificare ridondanti tipi, come fanno i linguaggi dinamici. D'altra parte, l'inferenza statica deduce tipi e altre proprietà del codice, dando il meglio sia del mondo statico che di quello dinamico.
  • Il motore di inferenza del tipo in Rust è piuttosto intelligente. Fa di più che guardare il tipo di valore r durante un'inizializzazione. Guarda anche come la variabile viene utilizzata in seguito per inferire il suo tipo.
  • Swift utilizza l'inferenza di tipo per elaborare il tipo appropriato. Inferenza del tipo consente a un compilatore di dedurre automaticamente il tipo di un'espressione particolare quando compila il codice, semplicemente esaminando i valori forniti.

3
In C #, le linee guida generali dicono di non usare semprevar perché a volte può danneggiare la leggibilità.
Mephy,

5
"... o perché diciamo che Java o le vecchie buone lingue non hanno deduzioni di tipo" Le ragioni sono probabilmente storiche; ML apparve 1 anno dopo C secondo Wikipedia e presentava un'inferenza di tipo. Java stava cercando di attrarre gli sviluppatori C ++ . Il C ++ è iniziato come un'estensione di C e le preoccupazioni principali di C erano essere un wrapper portatile sull'assemblaggio ed essere facili da compilare. Per quanto valga la pena, ho letto che il sottotipo rende indecifrabile l'inferenza di tipo anche nel caso generale.
Doval,

3
@Doval Scala sembra fare un buon lavoro nel dedurre i tipi, per un linguaggio che supporta l'eredità dei sottotipi. Non è buono come nessuna delle lingue della famiglia ML, ma probabilmente è buono come si potrebbe chiedere, dato il design della lingua.
KChaloux,

12
Vale la pena fare una distinzione tra la deduzione del tipo (monodirezionale, come C # vare C ++ auto) e l' inferenza del tipo (bidirezionale, come Haskell let). Nel primo caso, il tipo di un nome può essere dedotto solo dal suo inizializzatore: i suoi usi devono seguire il tipo di nome. In quest'ultimo caso, il tipo di un nome può essere dedotto anche dai suoi usi, il che è utile in quanto è possibile scrivere semplicemente []per una sequenza vuota, indipendentemente dal tipo di elemento o newEmptyMVarper un nuovo riferimento null mutable, indipendentemente dal referente genere.
Jon Purdy,

L'inferenza del tipo può essere incredibilmente complicata da implementare, specialmente in modo efficiente. Le persone non vogliono che la loro complessità di compilazione subisca l'esplosione esponenziale di espressioni più complesse. Lettura interessante: cocoawithlove.com/blog/2016/07/12/type-checker-issues.html
Alexander - Monica,

Risposte:


43

Il sistema di tipi di Haskell è completamente inferibile (lasciando da parte la ricorsione polimorfica, alcune estensioni del linguaggio e la temuta restrizione del monomorfismo ), ma i programmatori forniscono ancora frequentemente annotazioni di tipo nel codice sorgente anche quando non sono necessarie. Perché?

  1. Le annotazioni dei tipi servono come documentazione . Ciò è particolarmente vero con tipi espressivi come quelli di Haskell. Dato il nome di una funzione e il suo tipo, di solito si può avere una buona idea di cosa fa la funzione, specialmente quando la funzione è parametricamente polimorfica.
  2. Le annotazioni dei tipi possono favorire lo sviluppo . Scrivere una firma di tipo prima di scrivere il corpo di una funzione sembra un po 'come lo sviluppo test-driven. Nella mia esperienza, una volta compilata una funzione Haskell, di solito funziona per la prima volta. (Naturalmente, questo non elimina la necessità di test automatici!)
  3. I tipi espliciti possono aiutarti a verificare i tuoi presupposti . Quando sto cercando di capire un codice che già funziona, lo aggiungo spesso a quelle che credo siano le annotazioni di tipo corrette. Se il codice viene ancora compilato, so di averlo capito. In caso contrario, ho letto il messaggio di errore.
  4. Le firme di tipo consentono di specializzare le funzioni polimorfiche . Molto occasionalmente, un'API è più espressiva o utile se determinate funzioni non sono polimorfiche. Il compilatore non si lamenterà se si assegna a una funzione un tipo meno generale di quanto sarebbe dedotto. L'esempio classico è map :: (a -> b) -> [a] -> [b]. La sua forma più generale (fmap :: Functor f => (a -> b) -> f a -> f b ) si applica a tutti gli Functors, non solo agli elenchi. Ma si pensava che mapsarebbe stato più facile da capire per i principianti, quindi vive accanto al fratello maggiore.

Nel complesso, i lati negativi di un sistema staticamente tipizzato ma inferibile sono più o meno gli aspetti negativi della tipizzazione statica in generale, una discussione ben consumata su questo sito e altri (Googling "svantaggi della tipizzazione statica" ti porterà centinaia di pagine di guerre di fiamma). Naturalmente, alcuni di questi svantaggi sono migliorati dalla minore quantità di annotazioni di tipo in un sistema inferibile. Inoltre, l'inferenza di tipo ha i suoi vantaggi: sviluppo guidato dal foro non sarebbe possibile senza l'inferenza del tipo.

Java * dimostra che un linguaggio che richiede troppe annotazioni di tipo diventa fastidioso, ma con troppo pochi perdi i vantaggi che ho descritto sopra. Le lingue con deduzione di tipo opt-out raggiungono un piacevole equilibrio tra i due estremi.

* Anche Java, quel grande capro espiatorio, esegue una certa quantità di inferenza di tipo locale . In una dichiarazione come Map<String, Integer> = new HashMap<>();, non è necessario specificare il tipo generico del costruttore. D'altra parte, i linguaggi in stile ML sono generalmente inferibili a livello globale .


2
Ma non sei un principiante;) Devi avere una comprensione delle classi e dei tipi di tipo prima di "ottenere" davvero Functor. Elenca e ha mapmaggiori probabilità di avere familiarità con gli Haskeller non esperti.
Benjamin Hodgson,

1
Considerereste C # varcome un esempio di inferenza di tipo?
Benjamin Hodgson,

1
Sì, C # varè un esempio corretto.
Doval,

1
Il sito sta già segnalando questa discussione come troppo lunga, quindi questa sarà la mia ultima risposta. Nel primo il compilatore deve determinare il tipo di x; in quest'ultimo non c'è alcun tipo da determinare, tutti i tipi sono noti e devi solo verificare che l'espressione abbia un senso. La differenza diventa più importante man mano che passi esempi banali e xviene utilizzata in più punti; il compilatore deve quindi eseguire un controllo incrociato delle posizioni in cui xviene utilizzato per determinare 1) è possibile assegnare xun tipo in modo che il codice digiti check? 2) Se lo è, qual è il tipo più generale che possiamo assegnarlo?
Doval,

1
Si noti che la new HashMap<>();sintassi è stata aggiunta solo in Java 7 e le lambdas in Java 8 consentono un bel po 'di inferenza di tipo "reale".
Michael Borgwardt,

8

In C #, l'inferenza del tipo si verifica in fase di compilazione, quindi il costo di runtime è zero.

Per motivi di stile, varviene utilizzato per situazioni in cui è scomodo o non necessario specificare manualmente il tipo. Linq è una di queste situazioni. Un altro è:

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

senza la quale ripeteresti il ​​tuo nome di tipo molto lungo (e i parametri di tipo) piuttosto che dire semplicemente var.

Utilizzare il nome effettivo del tipo quando esplicito migliora la chiarezza del codice.

Ci sono alcune situazioni in cui l'inferenza del tipo non può essere usata, come dichiarazioni di variabili membro i cui valori sono impostati al momento della costruzione, o in cui si desidera davvero che intellisense funzioni correttamente (l'IDE di Hackerrank non intellisense i membri della variabile a meno che non si dichiari esplicitamente il tipo) .


16
"In C #, l'inferenza del tipo si verifica in fase di compilazione, quindi il costo di runtime è zero." L'inferenza del tipo si verifica in fase di compilazione per definizione, quindi è il caso in ogni lingua.
Doval,

Molto meglio.
Robert Harvey,

1
@Doval è possibile eseguire l'inferenza del tipo durante la compilazione JIT, che ha chiaramente un costo di runtime.
Jules,

6
@Jules Se sei arrivato al punto di eseguire il programma, è già stato verificato il tipo; non è rimasto nulla da dedurre. Quello di cui stai parlando non è in genere chiamato inferenza di tipo, anche se non sono sicuro di quale sia il termine corretto.
Doval,

7

Buona domanda!

  1. Poiché il tipo non è esplicitamente annotato, a volte può rendere il codice più difficile da leggere, portando a più bug. Naturalmente usato correttamente rende il codice più pulito e più leggibile. Se sei un pessimista e pensi che la maggior parte dei programmatori sia cattiva (o lavori dove la maggior parte dei programmatori è cattiva), questa sarà una perdita netta.
  2. Mentre l'algoritmo di inferenza del tipo è relativamente semplice, non è gratuito . Questo genere di cose aumenta leggermente il tempo di compilazione.
  3. Poiché il tipo non è esplicitamente annotato, il tuo IDE non può indovinare anche cosa stai cercando di fare, danneggiando il completamento automatico e aiutanti simili durante il processo di dichiarazione.
  4. In combinazione con i sovraccarichi di funzione, è possibile che occasionalmente si verifichino situazioni in cui l'algoritmo di inferenza del tipo non è in grado di decidere quale percorso intraprendere, portando a brutte annotazioni sullo stile del casting. (Questo succede un po 'con la sintassi della funzione anonima di C # per esempio).

E ci sono più lingue esoteriche che non possono fare la loro stranezza senza un'annotazione esplicita del tipo. Finora, non ne conosco nessuno che sia abbastanza comune / popolare / promettente da menzionare se non per caso.


1
il completamento automatico non gliene frega niente dell'inferenza di tipo. Non importa come è stato deciso il tipo, solo ciò che ha il tipo. E i problemi con i lambda di C # non hanno nulla a che fare con l'inferenza, sono perché i lambda sono polimorfici ma il sistema dei tipi non può farcela - il che non ha nulla a che fare con l'inferenza.
DeadMG

2
@deadmg - certo, una volta che la variabile è stata dichiarata non ha alcun problema, ma mentre stai digitando la dichiarazione della variabile, non può restringere le opzioni del lato destro al tipo dichiarato poiché non esiste un tipo dichiarato.
Telastyn,

@deadmg - per quanto riguarda gli lambda, non sono molto chiaro su cosa intendi, ma sono abbastanza sicuro che non sia del tutto corretto in base alla mia comprensione di come funzionano le cose. Anche qualcosa di semplice come var foo = x => x;fallisce perché la lingua ha bisogno di inferire xqui e non ha nulla da fare. Quando vengono creati i lambda, vengono creati quando i delegati digitati in modo esplicito Func<int, int> foo = x => xvengono incorporati in CIL come Func<int, int> foo = new Func<int, int>(x=>x);dove viene generato il lambda come funzione generata e tipizzata in modo esplicito. Per me, dedurre il tipo di xè parte integrante del tipo di inferenza ...
Telastyn,

3
@Telastyn Non è vero che la lingua non ha nulla da fare. Tutte le informazioni necessarie per digitare l'espressione x => xsono contenute all'interno della stessa lambda - non fanno riferimento a nessuna variabile dall'ambito circostante. Qualsiasi linguaggio funzionale sano di mente avrebbe correttamente dedurre che il suo tipo è "per tutti i tipi a, a -> a". Penso che ciò che DeadMG stia cercando di dire è che il sistema di tipi di C # manca della proprietà di tipo principale, il che significa che è sempre possibile capire il tipo più generale per qualsiasi espressione. È una proprietà molto facile da distruggere.
Doval,

@Doval - enh ... non ci sono oggetti funzione generici in C #, che penso siano leggermente diversi da ciò di cui entrambi state parlando anche se porta allo stesso problema.
Telastyn,

4

Mi sembra fantastico, ma mi chiedo se ci siano dei compromessi o perché diciamo che Java o le vecchie buone lingue non hanno inferenza di tipo

Java sembra essere l'eccezione piuttosto che la regola qui. Anche il C ++ (che credo sia qualificato come "buon vecchio linguaggio" :)) supporta l'inferenza di tipo con la autoparola chiave dallo standard C ++ 11. Funziona non solo con la dichiarazione variabile, ma anche come tipo di ritorno di funzione, che è particolarmente utile con alcune funzioni di template complesse.

La digitazione implicita e l'inferenza di tipo hanno molti buoni casi d'uso, e ci sono anche alcuni casi d'uso in cui davvero non dovresti farlo. Talvolta è questione di gusti e anche argomento di dibattito.

Ma che ci sono indubbiamente buoni casi d'uso, è di per sé una ragione legittima per qualsiasi linguaggio per implementarlo. Inoltre, non è difficile implementare funzionalità, nessuna penalità di runtime e non influisce in modo significativo sul tempo di compilazione.

Non vedo un vero svantaggio nel dare l'opportunità allo sviluppatore di usare l'inferenza del tipo.

Alcuni rispondenti ribadiscono quanto a volte la digitazione esplicita sia buona e hanno certamente ragione. Ma non supportare la digitazione implicita significherebbe che un linguaggio impone sempre la digitazione esplicita.

Quindi il vero svantaggio è quando una lingua non supporta la digitazione implicita, perché con ciò afferma che non c'è motivo legittimo per lo sviluppatore di usarla, il che non è vero.


1

La principale distinzione tra un sistema di inferenza di tipo Hindley-Milner e un'inferenza di tipo Go è la direzione del flusso di informazioni. In HM, digitare i flussi di informazioni in avanti e indietro tramite l'unificazione; in Vai, digita solo i flussi di informazioni in avanti: calcola solo le sostituzioni in avanti.

L'inferenza di tipo HM è una splendida innovazione che funziona bene con linguaggi funzionali tipicamente polimorficamente, ma gli autori di Go probabilmente sosterrebbero che cerca di fare troppo:

  1. Il fatto che l'informazione fluisca sia in avanti che all'indietro significa che l'inferenza di tipo HM è un affare molto non locale; in assenza di annotazioni di tipo, ogni riga di codice nel programma potrebbe contribuire alla digitazione di una singola riga di codice. Se hai solo una sostituzione diretta, sai che l'origine di un errore di tipo deve essere nel codice che precede l'errore; non il codice che viene dopo.

  2. Con l'inferenza di tipo HM, tendi a pensare vincoli: quando si utilizza un tipo, si vincolano i possibili tipi che può avere. Alla fine della giornata, potrebbero esserci alcune variabili di tipo che sono lasciate totalmente senza vincoli. L'intuizione dell'inferenza del tipo HM è che quei tipi non contano davvero, e quindi vengono trasformati in variabili polimorfiche. Tuttavia, questo polimorfismo extra può essere indesiderabile per una serie di ragioni. In primo luogo, come alcune persone hanno sottolineato, questo polimorfismo extra potrebbe essere indesiderabile: HM conclude un tipo fasullo, polimorfico per un codice fasullo, e porta a strani errori in seguito. In secondo luogo, quando un tipo viene lasciato polimorfico, ciò può avere conseguenze sul comportamento in fase di esecuzione. Ad esempio, un risultato intermedio eccessivamente polimorfico è il motivo per cui 'spettacolo. leggere 'è considerato ambiguo in Haskell; come altro esempio,


2
Sfortunatamente, questa sembra una buona risposta a una domanda diversa. OP chiede "Inferenza del tipo di stile Go" rispetto alle lingue che non hanno alcuna inferenza di tipo, non "Stile di tipo Go" rispetto a HM.
Ixrec,

-2

Fa male la leggibilità.

Confrontare

var stuff = someComplexFunction()

vs

List<BusinessObject> stuff = someComplexFunction()

È particolarmente un problema quando si legge all'esterno di un IDE come su github.


4
questo non sembra offrire nulla di sostanziale rispetto ai punti formulati e spiegati nelle precedenti 6 risposte
moscerino del

Se usi il nome proprio invece di "complesso illeggibile", varpuoi usarlo senza danneggiare la leggibilità. var objects = GetBusinessObjects();
Fabio,

Ok ma allora stai perdendo il sistema di tipi nei nomi delle variabili. È come una notazione ungherese. Dopo i refactoring è più probabile che finiscano con nomi che non corrispondono al codice. In generale, lo stile funzionale ti spinge a perdere i vantaggi naturali di spaziatura che le classi offrono. Quindi finisci con più squareArea () invece di square.area ().
miguel,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.