Ci sono alcune questioni importanti che penso che tutte le risposte esistenti abbiano perso.
Digitazione debole significa consentire l'accesso alla rappresentazione sottostante. In C, posso creare un puntatore a caratteri, quindi dire al compilatore che voglio usarlo come puntatore a numeri interi:
char sz[] = "abcdefg";
int *i = (int *)sz;
Su una piattaforma little-endian con numeri interi a 32 bit, questo i
diventa un array di numeri 0x64636261
e 0x00676665
. In effetti, puoi persino lanciare i puntatori stessi su numeri interi (della dimensione appropriata):
intptr_t i = (intptr_t)&sz;
E ovviamente questo significa che posso sovrascrivere la memoria ovunque nel sistema. *
char *spam = (char *)0x12345678
spam[0] = 0;
* Naturalmente i moderni sistemi operativi usano la memoria virtuale e la protezione della pagina in modo che io possa solo sovrascrivere la memoria del mio processo, ma non c'è nulla nella stessa C che offra tale protezione, come chiunque può mai aver codificato, diciamo, Classic Mac OS o Win16 può dirti.
Il Lisp tradizionale permetteva simili tipi di pirateria informatica; su alcune piattaforme, i float e le contro celle a doppia parola erano dello stesso tipo, e potevi semplicemente passare l'uno a una funzione aspettandoti l'altro e avrebbe "funzionato".
La maggior parte delle lingue oggi non è così debole come lo erano C e Lisp, ma molte di esse sono ancora in qualche modo trapelate. Ad esempio, qualsiasi linguaggio OO che ha un "downcast" non selezionato, * è una perdita di tipo: stai essenzialmente dicendo al compilatore "So che non ti ho dato abbastanza informazioni per sapere che è sicuro, ma sono abbastanza sicuro è "quando il punto centrale di un sistema di tipi è che il compilatore ha sempre abbastanza informazioni per sapere cosa è sicuro.
* Un downcast verificato non rende il sistema di tipi di lingua più debole solo perché sposta il controllo in fase di esecuzione. In tal caso, il polimorfismo dei sottotipi (ovvero chiamate di funzione virtuali o completamente dinamiche) sarebbe la stessa violazione del sistema dei tipi, e non credo che nessuno voglia dirlo.
Pochissimi linguaggi di "scripting" sono deboli in questo senso. Anche in Perl o Tcl, non puoi prendere una stringa e interpretare i suoi byte come un intero. * Ma vale la pena notare che in CPython (e allo stesso modo per molti altri interpreti per molte lingue), se sei davvero persistente, tu può utilizzare ctypes
per caricare libpython
, eseguire il cast di un oggetto su id
a POINTER(Py_Object)
e forzare la fuoriuscita del sistema di tipi. Il fatto che ciò renda debole il sistema dei tipi o meno dipende dai tuoi casi d'uso: se stai cercando di implementare un sandbox di esecuzione limitata in lingua per garantire la sicurezza, devi affrontare questo tipo di escape ...
* È possibile utilizzare una funzione come struct.unpack
leggere i byte e creare un nuovo int su "come C rappresenterebbe questi byte", ma ovviamente non ha perdite; anche Haskell lo consente.
Nel frattempo, la conversione implicita è davvero una cosa diversa da un sistema di tipo debole o che perde.
Ogni lingua, anche Haskell, ha funzioni per, diciamo, convertire un numero intero in una stringa o in un float. Ma alcune lingue eseguiranno automaticamente alcune di quelle conversioni per te, ad esempio in C, se chiami una funzione che vuole una float
e la passi int
, viene convertita per te. Questo può sicuramente portare a bug con, ad esempio, overflow imprevisti, ma non sono gli stessi tipi di bug che si ottengono da un sistema di tipo debole. E C non sta davvero diventando più debole qui; puoi aggiungere un int e un float in Haskell, o anche concatenare un float a una stringa, devi solo farlo in modo più esplicito.
E con linguaggi dinamici, questo è piuttosto oscuro. Non esiste una cosa come "una funzione che vuole un float" in Python o Perl. Ma ci sono funzioni sovraccariche che fanno cose diverse con tipi diversi, e c'è un forte senso intuitivo che, ad esempio, aggiungere una stringa a qualcos'altro è "una funzione che vuole una stringa". In tal senso, Perl, Tcl e JavaScript sembrano fare molte conversioni implicite ( "a" + 1
ti dà "a1"
), mentre Python ne fa molte meno ( "a" + 1
solleva un'eccezione, ma 1.0 + 1
ti dà 2.0
*). È solo difficile esprimere questo senso in termini formali: perché non dovrebbe esserci uno +
che prende una stringa e un int, quando ci sono ovviamente altre funzioni, come l'indicizzazione, che lo fanno?
* In realtà, nel moderno Python, ciò può essere spiegato in termini di sottotipo OO, poiché isinstance(2, numbers.Real)
è vero. Non penso che abbia senso in quale 2
istanza sia del tipo stringa in Perl o JavaScript ... anche se in Tcl, in realtà lo è, poiché tutto è un'istanza di stringa.
Infine, c'è un'altra definizione, completamente ortogonale, di tipizzazione "forte" contro "debole", dove "forte" significa potente / flessibile / espressivo.
Ad esempio, Haskell ti consente di definire un tipo che è un numero, una stringa, un elenco di questo tipo o una mappa dalle stringhe a questo tipo, che è un modo perfetto per rappresentare tutto ciò che può essere decodificato da JSON. Non c'è modo di definire un tale tipo in Java. Ma almeno Java ha tipi parametrici (generici), quindi puoi scrivere una funzione che prende un Elenco di T e sapere che gli elementi sono di tipo T; altre lingue, come i primi Java, ti costrinsero a usare un Elenco di oggetti e downcast. Ma almeno Java ti consente di creare nuovi tipi con i loro metodi; C ti consente solo di creare strutture. E BCPL non aveva nemmeno quello. E così via fino all'assemblaggio, in cui gli unici tipi hanno lunghezze di bit diverse.
Quindi, in questo senso, il sistema di tipi di Haskell è più forte di quello moderno di Java, che è più forte di quello precedente di Java, che è più forte di quello di C, che è più forte di quello di BCPL.
Quindi, dove si inserisce Python in quello spettro? È un po 'complicato. In molti casi, la digitazione di anatre ti consente di simulare tutto ciò che puoi fare in Haskell e anche alcune cose che non puoi; certo, gli errori vengono rilevati in fase di esecuzione anziché in fase di compilazione, ma vengono comunque rilevati. Tuttavia, ci sono casi in cui la tipizzazione anatra non è sufficiente. Ad esempio, in Haskell, puoi dire che un elenco vuoto di ints è un elenco di ints, quindi puoi decidere che la riduzione +
su quell'elenco dovrebbe restituire 0 *; in Python, un elenco vuoto è un elenco vuoto; non ci sono informazioni sul tipo per aiutarti a decidere cosa +
dovrebbe fare la riduzione su di essa.
* In effetti, Haskell non ti consente di farlo; se si chiama la funzione di riduzione che non accetta un valore iniziale in un elenco vuoto, viene visualizzato un errore. Ma il suo sistema di tipi è abbastanza potente da permetterti di farlo funzionare, e quello di Python no.