In che modo la tipizzazione statica è davvero utile nei progetti più grandi?


9

Mentre curioso sulla pagina principale di un sito di un linguaggio di programmazione di scripting, ho incontrato questo passaggio:

Quando un sistema diventa troppo grande da tenere in testa, puoi aggiungere tipi statici.

Questo mi ha fatto ricordare che in molte guerre di religione tra linguaggi compilati statici (come Java) e linguaggi dinamici e interpretati (principalmente Python perché è più usato, ma è un "problema" condiviso con la maggior parte dei linguaggi di scripting), una delle lamentele di staticamente I fan dei linguaggi tipizzati su linguaggi tipicamente dinamici è che non si adattano bene a progetti più grandi perché "un giorno dimenticherai il tipo di ritorno di una funzione e dovrai cercarlo, mentre con i linguaggi tipicamente statici tutto è esplicitamente dichiarato ".

Non ho mai capito dichiarazioni come questa. Ad essere onesti, anche se dichiari il tipo restituito di una funzione, puoi e lo dimenticherai dopo aver scritto molte righe di codice, e dovrai comunque tornare alla riga in cui è dichiarato usando la funzione di ricerca di il tuo editor di testo per verificarlo.

Inoltre, poiché vengono dichiarate le funzioni type funcname()..., senza sapere typeche dovrai cercare su ogni riga in cui viene chiamata la funzione, perché sai solo funcname, mentre in Python e simili puoi solo cercare def funcnameo function funcnameche accade solo una volta, a la dichiarazione.

Inoltre, con REPL è banale testare una funzione per il suo tipo restituito con input diversi, mentre con linguaggi tipicamente statici dovresti aggiungere alcune righe di codice e ricompilare tutto solo per sapere il tipo dichiarato.

Quindi, oltre a conoscere il tipo restituito di una funzione che chiaramente non è un punto di forza di linguaggi tipicamente statici, in che modo la tipizzazione statica è davvero utile in progetti più grandi?



2
se leggi le risposte all'altra domanda, probabilmente otterrai le risposte di cui hai bisogno per questa, fondamentalmente stanno chiedendo la stessa cosa da diverse prospettive :)
Sara

1
Swift e playground sono una REPL di una lingua tipizzata staticamente.
daven11,

2
Le lingue non sono compilate, le implementazioni lo sono. Il modo di scrivere un REPL per un linguaggio "compilato" è scrivere qualcosa che possa interpretare il linguaggio, o almeno compilarlo ed eseguirlo riga per riga, mantenendo lo stato necessario. Inoltre, Java 9 verrà fornito con un REPL.
Sebastian Redl,

2
@ user6245072: Ecco come creare un REPL per un interprete: leggere il codice, inviarlo all'interprete, stampare il risultato. Ecco come creare un REPL per un compilatore: leggere il codice, inviarlo al compilatore, eseguire il codice compilato , stampare il risultato. Facile come una torta. Questo è esattamente ciò che fanno FSi (REPL F♯), GHCi (REPL di GHC Haskell), REPL di Scala e Cling.
Jörg W Mittag,

Risposte:


21

Inoltre, con REPL è banale testare una funzione per il suo tipo di ritorno con input diversi

Non è banale. Non è banale affatto . È solo banale farlo per funzioni banali.

Ad esempio, è possibile definire banalmente una funzione in cui il tipo restituito dipende interamente dal tipo di input.

getAnswer(v) {
 return v.answer
}

In questo caso, in getAnswerrealtà non ha un solo tipo restituito. Non esiste un test che puoi mai scrivere che lo chiama con un input di esempio per sapere qual è il tipo di ritorno. Dipenderà sempre dall'argomento reale. In fase di esecuzione.

E questo non include nemmeno funzioni che, ad esempio, eseguono ricerche nel database. Oppure fai le cose in base all'input dell'utente. Oppure cerca le variabili globali, che sono ovviamente di tipo dinamico. O cambia il tipo di ritorno in casi casuali. Per non parlare della necessità di testare manualmente ogni singola singola funzione ogni volta.

getAnswer(x, y) {
   if (x + y.answer == 13)
       return 1;
   return "1";
}

Fondamentalmente, dimostrare il tipo di ritorno della funzione nel caso generale è letteralmente matematicamente impossibile (Halting Problem). L' unico modo per garantire il tipo restituito è limitare l'input in modo che la risposta a questa domanda non rientri nel dominio del problema di interruzione impedendo i programmi che non sono provabili, ed è ciò che fa la tipizzazione statica.

Inoltre, poiché le funzioni sono dichiarate con type funcname () ..., senza conoscere il tipo dovrai cercare su ogni riga in cui viene chiamata la funzione, perché conosci solo funcname, mentre in Python e simili puoi solo cercare def funcname o function funcname che si verifica solo una volta, alla dichiarazione.

I linguaggi tipizzati staticamente hanno cose chiamate "strumenti". Sono programmi che ti aiutano a fare le cose con il tuo codice sorgente. In questo caso, farei semplicemente clic con il pulsante destro del mouse e vai a definizione, grazie a Resharper. Oppure usa la scorciatoia da tastiera. Oppure passa il mouse e mi dirà quali sono i tipi coinvolti. Non mi interessa minimamente dei file grepping. Un editor di testo da solo è uno strumento patetico per la modifica del codice sorgente del programma.

Dalla memoria, def funcnamenon sarebbe sufficiente in Python, poiché la funzione potrebbe essere riassegnata arbitrariamente. O potrebbe essere dichiarato più volte in più moduli. O in classe. Eccetera.

e dovrai comunque tornare alla riga in cui è dichiarato usando la funzione di ricerca del tuo editor di testo per verificarlo.

La ricerca di file per il nome della funzione è un'operazione terribile primitiva che non dovrebbe mai essere richiesta. Ciò rappresenta un fallimento fondamentale dell'ambiente e degli strumenti. Il fatto che potresti anche considerare la necessità di una ricerca di testo in Python è un punto enorme contro Python.


2
Ad essere onesti, quegli "strumenti" sono stati inventati in linguaggi dinamici e i linguaggi dinamici li avevano molto prima di quelli statici. Vai a definizione, completamento del codice, refactoring automatizzato ecc. Esistevano negli IDE grafici Lisp e Smalltalk prima che i linguaggi statici presentassero persino grafica o IDE, per non parlare degli IDE grafici.
Jörg W Mittag,

Conoscere il tipo di funzioni di ritorno non sempre ti dice quali funzioni FANNO . Invece di scrivere tipi potresti aver scritto test di documenti con valori di esempio. per esempio, confronta (parole 'alcune parole oue') => ['alcuni', 'parole', 'oeu'] con (parole stringa) -> [stringa], (zip {abc} [1..3]) => [(a, 1), (b, 2), (c, 3)] con il suo tipo.
aoeu256,

18

Pensa a un progetto con molti programmatori, che è cambiato nel corso degli anni. Devi mantenerlo. C'è una funzione

getAnswer(v) {
 return v.answer
}

Cosa diavolo fa? Cosa v? Da dove viene l'elemento answer?

getAnswer(v : AnswerBot) {
  return v.answer
}

Ora abbiamo qualche informazione in più -; ha bisogno di un tipo di AnswerBot.

Se andiamo in una lingua di classe possiamo dire

class AnswerBot {
  var answer : String
  func getAnswer() -> String {
    return answer
  }
}

Ora possiamo avere una variabile di tipo AnswerBote chiamare il metodo getAnswere tutti sanno cosa fa. Qualsiasi modifica viene rilevata dal compilatore prima di eseguire qualsiasi test di runtime. Ci sono molti altri esempi, ma forse questo ti dà l'idea?


1
Sembra già più chiaro - a meno che tu non sottolinei che una funzione del genere non ha motivo di esistere, ma questo è ovviamente solo un esempio.
user6245072,

Questo è il problema quando hai più programmatori su un grande progetto, funzioni del genere esistono (e peggio ancora), è roba da incubi. considera anche che le funzioni nei linguaggi dinamici si trovano nello spazio dei nomi globale, quindi nel tempo potresti avere un paio di funzioni getAnswer - e funzionano entrambe e sono diverse perché caricate in momenti diversi.
daven11,

1
Immagino sia un fraintendimento della programmazione funzionale che lo causa. Tuttavia, cosa intendi dicendo che sono nello spazio dei nomi globale?
user6245072,

3
"Le funzioni nei linguaggi dinamici sono di default nello spazio dei nomi globale" questo è un dettaglio specifico della lingua e non un vincolo causato dalla digitazione dinamica.
Sara

2
@ daven11 "Sto pensando a javascript qui", in effetti, ma altri linguaggi dinamici hanno spazi dei nomi / moduli / pacchetti effettivi e possono avvisarti di ridefinizioni. Potresti generalizzare un po 'troppo.
coredump,

10

Sembra che tu abbia alcune idee sbagliate su come lavorare con grandi progetti statici che potrebbero offuscare il tuo giudizio. Ecco alcuni suggerimenti:

anche se dichiari il tipo restituito di una funzione, puoi e lo dimenticherai dopo aver scritto molte righe di codice e dovrai comunque tornare alla riga in cui è dichiarato usando la funzione di ricerca dell'editor di testo in controllalo.

La maggior parte delle persone che lavorano con linguaggi tipizzati staticamente usano un IDE per la lingua o un editor intelligente (come vim o emacs) che ha integrazione con strumenti specifici della lingua. Di solito esiste un modo rapido per trovare il tipo di funzione in questi strumenti. Ad esempio, con Eclipse su un progetto Java, ci sono due modi in cui normalmente si trova il tipo di un metodo:

  • Se voglio usare un metodo su un altro oggetto diverso da 'this', scrivo un riferimento e un punto (ad es. someVariable.) Ed Eclipse cerca il tipo di someVariablee fornisce un elenco a discesa di tutti i metodi definiti in quel tipo; mentre scorro verso il basso l'elenco, vengono visualizzati il ​​tipo e la documentazione di ciascuno mentre è selezionato. Nota che questo è molto difficile da ottenere con un linguaggio dinamico, perché è difficile (o in alcuni casi impossibile) per l'editor determinare quale sia il tipo di someVariable, quindi non può generare facilmente l'elenco corretto. Se voglio usare un metodo su thisposso semplicemente premere ctrl + spazio per ottenere lo stesso elenco (anche se in questo caso non è così difficile da ottenere per i linguaggi dinamici).
  • Se ho già un riferimento scritto su un metodo specifico, posso spostare il cursore del mouse su di esso e il tipo e la documentazione per il metodo vengono visualizzati in una descrizione comandi.

Come puoi vedere, questo è un po 'meglio degli strumenti tipici disponibili per i linguaggi dinamici (non che ciò sia impossibile nei linguaggi dinamici, poiché alcuni hanno una funzionalità IDE piuttosto buona - smalltalk è uno che ti viene in mente - ma è più difficile per un linguaggio dinamico e quindi meno probabile che sia disponibile).

Inoltre, poiché le funzioni sono dichiarate con type funcname () ..., senza conoscere il tipo dovrai cercare su ogni riga in cui viene chiamata la funzione, perché conosci solo funcname, mentre in Python e simili puoi solo cercare def funcname o function funcname che si verifica solo una volta, alla dichiarazione.

Gli strumenti di linguaggio statico in genere forniscono funzionalità di ricerca semantica, ovvero possono trovare con precisione definizioni e riferimenti a simboli particolari, senza la necessità di eseguire una ricerca di testo. Ad esempio, usando Eclipse per un progetto Java, posso evidenziare un simbolo nell'editor di testo e fare clic con il tasto destro del mouse e scegliere 'vai alla definizione' o 'trova riferimenti' per eseguire una di queste operazioni. Non è necessario cercare il testo di una definizione di funzione, perché il tuo editor sa già esattamente dove si trova.

Tuttavia, il contrario è che la ricerca di una definizione di metodo tramite testo non funziona davvero bene in un grande progetto dinamico come suggerisci, poiché in un tale progetto potrebbero esserci facilmente più metodi con lo stesso nome e probabilmente non hai strumenti prontamente disponibili per chiarire quale di essi stai invocando (perché tali strumenti sono difficili da scrivere nella migliore delle ipotesi o impossibili nel caso generale), quindi dovrai farlo a mano.

Inoltre, con REPL è banale testare una funzione per il suo tipo di ritorno con input diversi

Non è impossibile avere un REPL per una lingua tipizzata staticamente. Haskell è l'esempio che mi viene in mente, ma ci sono REPL anche per altre lingue tipizzate staticamente. Ma il punto è che non è necessario eseguire il codice per trovare il tipo restituito di una funzione in un linguaggio statico - può essere determinato mediante esame senza dover eseguire nulla.

mentre con linguaggi tipicamente statici dovresti aggiungere alcune righe di codice e ricompilare tutto solo per conoscere il tipo dichiarato.

È probabile che anche se avessi bisogno di farlo, non dovresti ricompilare tutto . La maggior parte dei linguaggi statici moderni ha compilatori incrementali che compileranno solo la piccola parte del codice che è cambiata, in modo da poter ottenere un feedback quasi istantaneo per errori di tipo se ne fai uno. Eclipse / Java, ad esempio, evidenzierà gli errori di tipo mentre li stai ancora digitando .


4
You seem to have a few misconceptions about working with large static projects that may be clouding your judgement.Bene, ho solo 14 anni e programma solo da meno di un anno su Android, quindi è possibile immagino.
user6245072,

1
Anche senza un IDE, se rimuovi un metodo da una classe in Java e ci sono cose che dipendono da quel metodo, qualsiasi compilatore Java ti darà un elenco di ogni linea che stava usando quel metodo. In Python, fallisce quando il codice in esecuzione chiama il metodo mancante. Uso regolarmente Java e Python e adoro Python per la velocità con cui riesci a far funzionare le cose e le cose interessanti che puoi fare che Java non supporta ma la realtà è che ho problemi nei programmi Python che semplicemente non accadono con (dritto) Java. Il refactoring in particolare è molto più difficile in Python.
JimmyJames,

6
  1. Perché i controlli statici sono più facili per le lingue tipizzate staticamente.
    • Come minimo, senza funzionalità di linguaggio dinamico, se si compila, quindi in fase di esecuzione non ci sono funzioni non risolte. Questo è comune nei progetti ADA e C sui microcontrollori. (I programmi del microcontrollore diventano grandi a volte ... come centinaia di kloc grandi.)
  2. I controlli di riferimento di compilazione statici sono un sottoinsieme di invarianti di funzioni, che in un linguaggio statico possono anche essere controllati in fase di compilazione.
  3. Le lingue statiche di solito hanno una maggiore trasparenza referenziale. Il risultato è che un nuovo sviluppatore può immergersi in un singolo file e comprendere alcune delle cose che stanno succedendo e correggere un bug o aggiungere una piccola funzionalità senza dover conoscere tutte le strane cose nella base di codice.

Confronta con dire, javascript, Ruby o Smalltalk, in cui gli sviluppatori ridefiniscono le funzionalità del linguaggio principale in fase di esecuzione. Questo rende più difficile la comprensione del grande progetto.

I progetti più grandi non hanno solo più persone, hanno più tempo. Abbastanza tempo per tutti da dimenticare o andare avanti.

Aneddoticamente, un mio conoscente ha una programmazione sicura "Job For Life" in Lisp. Nessuno tranne il team può comprendere la base di codice.


Anecdotally, an acquaintance of mine has a secure "Job For Life" programming in Lisp. Nobody except the team can understand the code-base.è davvero così male? La personalizzazione che hanno aggiunto non li aiuta a essere più produttivi?
user6245072,

@ user6245072 Potrebbe essere un vantaggio per le persone che lavorano attualmente lì, ma rende più difficile il reclutamento di nuove persone. Ci vuole più tempo per trovare qualcuno che già conosce una lingua non tradizionale o per insegnargli una che non conosce già. Ciò può rendere più difficile il ridimensionamento del progetto quando ha successo, o riprendersi dalle fluttuazioni: le persone si allontanano, vengono promosse in altre posizioni ... Dopo un po ', può anche essere uno svantaggio per gli specialisti stessi - una volta che hai scritto un linguaggio di nicchia per circa un decennio, potrebbe essere difficile passare a qualcosa di nuovo.
Hulk,

Non puoi semplicemente usare un tracciante per creare unit test dal programma Lisp in esecuzione? Come in Python puoi creare un decoratore (avverbio) chiamato print_args che accetta una funzione e restituisce una funzione modificata che stampa il suo argomento. È quindi possibile applicarlo a tutto il programma in sys.modules, sebbene un modo più semplice per farlo sia usare sys.set_trace.
aoeu256,

@ aoeu256 Non ho familiarità con le capacità dell'ambiente di runtime Lisp. Ma usavano pesantemente le macro, quindi nessun normale programmatore lisp poteva leggere il codice; È probabile che provare a fare cose "semplici" durante il runtime non funzioni a causa delle macro che cambiano tutto su Lisp.
Tim Williscroft,

@TimWilliscroft Potresti aspettare che tutte le macro vengano espanse prima di fare quel tipo di cose. Emacs ha molti tasti di scelta rapida che ti consentono di incorporare le macro di espansione (e forse le funzioni in linea).
aoeu256,

4

Non ho mai capito dichiarazioni come questa. Ad essere onesti, anche se dichiari il tipo restituito di una funzione, puoi e lo dimenticherai dopo aver scritto molte righe di codice, e dovrai comunque tornare alla riga in cui è dichiarato usando la funzione di ricerca di il tuo editor di testo per verificarlo.

Non si tratta di dimenticare il tipo restituito: questo accadrà sempre. Si tratta dello strumento in grado di farti sapere che hai dimenticato il tipo restituito.

Inoltre, poiché le funzioni sono dichiarate con il tipo funcname()..., senza sapere il tipo dovrai cercare su ogni riga in cui viene chiamata la funzione, perché sai solo funcname, mentre in Python e simili potresti cercare def funcnameo function funcnamecosa succede solo una volta , alla dichiarazione.

Questa è una questione di sintassi, completamente estranea alla tipizzazione statica.

La sintassi della famiglia C è davvero ostile quando vuoi cercare una dichiarazione senza avere strumenti specializzati a tua disposizione. Altre lingue non hanno questo problema. Vedi la sintassi della dichiarazione di Rust:

fn funcname(a: i32) -> i32

Inoltre, con REPL è banale testare una funzione per il suo tipo restituito con input diversi, mentre con linguaggi digitati staticamente dovresti aggiungere alcune righe di codice e ricompilare tutto solo per sapere il tipo dichiarato.

Qualsiasi lingua può essere interpretata e qualsiasi lingua può avere un REPL.


Quindi, oltre a conoscere il tipo di ritorno di una funzione che chiaramente non è un punto di forza dei linguaggi tipizzati staticamente, in che modo la tipizzazione statica è davvero utile nei progetti più grandi?

Risponderò in modo astratto.

Un programma è composto da varie operazioni e tali operazioni sono strutturate come sono a causa di alcune ipotesi fatte dallo sviluppatore.

Alcune ipotesi sono implicite e alcune sono esplicite. Alcune ipotesi riguardano un'operazione vicino a loro, altre riguardano un'operazione lontana da loro. Un'ipotesi è più facile da identificare quando viene espressa in modo esplicito e il più vicino possibile ai luoghi in cui conta il suo valore di verità.

Un bug è la manifestazione di un presupposto che esiste nel programma ma non è valido per alcuni casi. Per rintracciare un bug, dobbiamo identificare l'ipotesi errata. Per rimuovere il bug, è necessario rimuovere tale presupposto dal programma o modificare qualcosa in modo che il presupposto sia effettivamente valido.

Vorrei classificare le ipotesi in due tipi.

Il primo tipo sono le ipotesi che possono o non possono valere, a seconda degli input del programma. Per identificare un'ipotesi errata di questo tipo, dobbiamo cercare nello spazio di tutti i possibili input del programma. Utilizzando ipotesi educate e pensiero razionale, possiamo restringere il problema e cercare in uno spazio molto più piccolo. Tuttavia, man mano che un programma cresce anche un po ', il suo spazio di input iniziale cresce a un ritmo enorme, al punto che può essere considerato infinito per tutti gli scopi pratici.

Il secondo tipo sono le ipotesi che valgono sicuramente per tutti gli input o che sono decisamente errate per tutti gli input. Quando identifichiamo un'ipotesi di questo tipo come errata, non abbiamo nemmeno bisogno di eseguire il programma o testare alcun input. Quando identifichiamo un'ipotesi di questo tipo come corretta, abbiamo un sospetto in meno di cui preoccuparci quando stiamo rintracciando un bug ( qualsiasi bug). Pertanto, è utile avere quante più ipotesi possibili appartengano a questo tipo.

Per mettere un'ipotesi nella seconda categoria (sempre vera o sempre falsa, indipendentemente dagli input), abbiamo bisogno di una quantità minima di informazioni per essere disponibili nel luogo in cui viene fatta l'assunzione. Attraverso il codice sorgente di un programma, le informazioni diventano obsolete abbastanza rapidamente (ad esempio, molti compilatori non eseguono analisi interprocedurali, il che rende qualsiasi chiamata un limite rigido per la maggior parte delle informazioni). Abbiamo bisogno di un modo per mantenere aggiornate le informazioni richieste (valide e vicine).

Un modo è quello di avere la fonte di queste informazioni il più vicino possibile al luogo in cui verranno consumate, ma ciò può essere poco pratico per la maggior parte dei casi d'uso. Un altro modo è ripetere le informazioni frequentemente, rinnovandone la pertinenza attraverso il codice sorgente.

Come puoi già immaginare, i tipi statici sono esattamente questo: i fari di informazioni di tipo sparsi nel codice sorgente. Tali informazioni possono essere utilizzate per inserire la maggior parte delle ipotesi sulla correttezza del tipo nella seconda categoria, il che significa che quasi tutte le operazioni possono essere classificate come sempre corrette o sempre errate rispetto alla compatibilità dei tipi.

Quando i nostri tipi non sono corretti, l'analisi ci fa risparmiare tempo portando il bug alla nostra attenzione in anticipo piuttosto che in ritardo. Quando i nostri tipi sono corretti, l'analisi ci fa risparmiare tempo assicurandoci che quando si verifica un bug, possiamo immediatamente escludere errori di tipo.


3

Ricordi il vecchio adagio "garbage in, garbage out", beh, questo è ciò che la tipizzazione statica aiuta a prevenire. Non è una panacea universale, ma la severità su quale tipo di dati accetta e restituisce una routine significa che hai la certezza che stai lavorando correttamente con esso.

Pertanto, una routine getAnswer che restituisce un numero intero non sarà utile quando si tenta di utilizzarlo in una chiamata basata su stringa. La digitazione statica ti sta già dicendo di stare attenta, che probabilmente stai commettendo un errore. (e certo, puoi quindi sovrascriverlo, ma dovresti sapere esattamente che cosa stai facendo e specificarlo nel codice usando un cast. In generale, tuttavia, non vuoi farlo - hackerare in un piolo tondo in un buco quadrato non funziona mai bene alla fine)

Ora puoi andare oltre utilizzando tipi complessi, creando una classe con funzionalità minerale, puoi iniziare a passarli e improvvisamente ottieni molta più struttura nel tuo programma. I programmi strutturati sono molto più facili da far funzionare correttamente e anche da mantenere.


Non è necessario eseguire l'inferenza di tipo statico (pylint), è possibile fare l'inferenza di tipo dinamico chrislaffra.blogspot.com/2016/12/…, anch'essa eseguita dal compilatore JIT di PyPy. Esiste anche un'altra versione dell'inferenza di tipo dinamico in cui un computer posiziona casualmente oggetti falsi negli argomenti e vede cosa causa un errore. Il problema dell'arresto non ha importanza per il 99% dei casi, se impieghi troppo tempo basta fermare l'algoritmo (è così che Python gestisce la ricorsione infinita, ha un limite di ricorsione che può essere impostato).
aoeu256,
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.