Esistono modelli di progettazione possibili solo in linguaggi tipicamente dinamici come Python?


30

Ho letto una domanda correlata Esistono schemi di progettazione non necessari in linguaggi dinamici come Python? e ricordato questa citazione su Wikiquote.org

La cosa meravigliosa della digitazione dinamica è che ti consente di esprimere tutto ciò che è calcolabile. E i sistemi di tipo no: i sistemi di tipo sono in genere decidibili e ti limitano a un sottoinsieme. Le persone che preferiscono i sistemi di tipo statico dicono "va bene, è abbastanza buono; tutti i programmi interessanti che vuoi scrivere funzioneranno come tipi ”. Ma è ridicolo: una volta che hai un sistema di tipi, non sai nemmeno quali programmi interessanti ci sono.

--- Software Engineering Radio Episode 140: Newspeak e tipi collegabili con Gilad Bracha

Mi chiedo, ci sono utili modelli o strategie di progettazione che, usando la formulazione della citazione, "non funzionano come tipi"?


3
Ho trovato il doppio dispacciamento e il modello Visitatore molto difficili da realizzare in linguaggi tipicamente statici, ma facilmente realizzabili in linguaggi dinamici. Vedi questa risposta (e la domanda) per esempio: programmers.stackexchange.com/a/288153/122079
user3002473

7
Ovviamente. Qualsiasi modello che comporta la creazione di nuove classi in fase di esecuzione, ad esempio. (questo è possibile anche in Java, ma non in C ++; c'è una scala mobile di dinamismo).
user253751

1
Dipenderebbe molto da quanto sia sofisticato il tuo sistema di tipi :-) I linguaggi funzionali di solito funzionano abbastanza bene.
Bergi,

1
Tutti sembrano parlare di sistemi di tipo come Java e C # invece di Haskell o OCaml. Una lingua con un potente sistema di tipi può essere concisa come una lingua dinamica ma mantenere la sicurezza dei tipi.
Andrew dice Reinstate Monica il

@immibis È errato. I sistemi di tipo statico possono assolutamente creare nuove classi "dinamiche" in fase di esecuzione. Vedi il capitolo 33 delle basi pratiche per i linguaggi di programmazione.
gardenhead,

Risposte:


4

Tipi di prima classe

La digitazione dinamica significa che hai tipi di prima classe: puoi ispezionare, creare e archiviare i tipi in fase di esecuzione, inclusi i tipi della lingua. Significa anche che vengono digitati i valori , non le variabili .

Il linguaggio tipizzato staticamente può produrre codice che si basa anche su tipi dinamici, come l'invio di metodi, le classi di tipi, ecc. Ma in un modo generalmente invisibile al runtime. Nel migliore dei casi, ti danno un modo per eseguire l'introspezione. In alternativa, è possibile simulare i tipi come valori ma si dispone di un sistema di tipi dinamici ad hoc.

Tuttavia, i sistemi di tipo dinamico raramente hanno solo tipi di prima classe. Puoi avere simboli di prima classe, pacchetti di prima classe, di prima classe ... tutto. Ciò è in contrasto con la rigorosa separazione tra la lingua del compilatore e la lingua di runtime in linguaggi tipizzati staticamente. Ciò che il compilatore o l'interprete può fare anche il runtime può fare.

Ora, concordiamo sul fatto che l'inferenza del tipo è una buona cosa e che mi piace controllare il mio codice prima di eseguirlo. Tuttavia, mi piace anche essere in grado di produrre e compilare codice in fase di esecuzione. E adoro precompilare le cose anche in fase di compilazione. In una lingua tipizzata in modo dinamico, questo viene fatto con la stessa lingua. In OCaml hai il sistema di tipo modulo / funzione, che è diverso dal sistema di tipo principale, che è diverso dal linguaggio del preprocessore. In C ++, hai il linguaggio modello che non ha nulla a che fare con il linguaggio principale, che generalmente ignora i tipi durante l'esecuzione. E va bene in quella lingua, perché non vogliono fornire di più.

In ultima analisi, che non cambia in realtà ciò che tipo di software è possibile sviluppare, ma l'espressività cambia come li si sviluppa e se è difficile o no.

Patterns

I modelli che si basano su tipi dinamici sono modelli che coinvolgono ambienti dinamici: classi aperte, dispacciamento, database di oggetti in memoria, serializzazione, ecc. Cose semplici come i contenitori generici funzionano perché un vettore non dimentica in fase di esecuzione sul tipo di oggetti che contiene (non sono necessari tipi parametrici).

Ho cercato di introdurre i molti modi in cui il codice viene valutato in Common Lisp, nonché esempi di possibili analisi statiche (questo è SBCL). L'esempio sandbox compila un piccolo sottoinsieme di codice Lisp recuperato da un file separato. Per essere ragionevolmente sicuro, cambio il readtable, consento solo un sottoinsieme di simboli standard e avvolgo le cose con un timeout.

;;
;; Fetching systems, installing them, etc. 
;; ASDF and QL provide provide resp. a Make-like facility 
;; and system management inside the runtime: those are
;; not distinct programs.
;; Reflexivity allows to develop dedicated tools: for example,
;; being able to find the transitive reduction of dependencies
;; to parallelize builds. 
;; https://gitlab.common-lisp.net/xcvb/asdf-dependency-grovel
;;
(ql:quickload 'trivial-timeout)

;;
;; Readtables are part of the runtime.
;; See also NAMED-READTABLES.
;;
(defparameter *safe-readtable* (copy-readtable *readtable*))
(set-macro-character #\# nil t *safe-readtable*)
(set-macro-character #\: (lambda (&rest args)
                           (declare (ignore args))
                           (error "Colon character disabled."))
                     nil
                     *safe-readtable*)

;; eval-when is necessary when compiling the whole file.
;; This makes the result of the form available in the compile-time
;; environment. 
(eval-when (:compile-toplevel :load-toplevel :execute)
  (defvar +WHITELISTED-LISP-SYMBOLS+ 
    '(+ - * / lambda labels mod rem expt round 
      truncate floor ceiling values multiple-value-bind)))

;;
;; Read-time evaluation #.+WHITELISTED-LISP-SYMBOLS+
;; The same language is used to control the reader.
;;
(defpackage :sandbox
  (:import-from
   :common-lisp . #.+WHITELISTED-LISP-SYMBOLS+)
  (:export . #.+WHITELISTED-LISP-SYMBOLS+))

(declaim (inline read-sandbox))

(defun read-sandbox (stream &key (timeout 3))
  (declare (type (integer 0 10) timeout))
  (trivial-timeout:with-timeout (timeout)
    (let ((*read-eval* nil)
          (*readtable* *safe-readtable*)
          ;;
          ;; Packages are first-class: no possible name collision.
          ;;
          (package (make-package (gensym "SANDBOX") :use '(:sandbox))))
      (unwind-protect
           (let ((*package* package))
             (loop
                with stop = (gensym)
                for read = (read stream nil stop)
                until (eq read stop)
                ;;
                ;; Eval at runtime
                ;;
                for value = (eval read)
                ;;
                ;; Type checking
                ;;
                unless (functionp value)
                do (error "Not a function")
                ;; 
                ;; Compile at run-time
                ;;
                collect (compile nil value)))
        (delete-package package)))))

;;
;; Static type checking.
;; warning: Constant 50 conflicts with its asserted type (MOD 11)
;;
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in :timeout 50)))

;; get it right, this time
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in)))

#| /tmp/plugin.lisp
(lambda (x) (+ (* 3 x) 100))
(lambda (a b c) (* a b))
|#

(read-sandbox-file #P"/tmp/plugin.lisp")

;; 
;; caught COMMON-LISP:STYLE-WARNING:
;;   The variable C is defined but never used.
;;

(#<FUNCTION (LAMBDA (#:X)) {10068B008B}>
 #<FUNCTION (LAMBDA (#:A #:B #:C)) {10068D484B}>)

Niente sopra è "impossibile" da fare con altre lingue. L'approccio plug-in in Blender, in software musicali o IDE per linguaggi compilati staticamente che eseguono la compilazione al volo, ecc. Invece di strumenti esterni, i linguaggi dinamici favoriscono strumenti che fanno uso di informazioni che sono già lì. Tutti i chiamanti conosciuti di FOO? tutte le sottoclassi di BAR? tutti i metodi specializzati dalla classe ZOT? si tratta di dati interiorizzati. I tipi sono solo un altro aspetto di questo.


(vedi anche: CFFI )


39

Risposta breve: no, perché l'equivalenza di Turing.

Risposta lunga: Questo ragazzo è un troll. Sebbene sia vero che i sistemi di tipi "ti limitano a un sottoinsieme", le cose esterne a quel sottoinsieme sono, per definizione, cose che non funzionano.

Tutto ciò che sei in grado di fare in qualsiasi linguaggio di programmazione completo di Turing (che è un linguaggio progettato per la programmazione generale, oltre a molti altri che non lo sono; è una barra piuttosto bassa da cancellare e ci sono diversi esempi di un sistema che sta diventando Turing- completare involontariamente) è possibile eseguire qualsiasi altro linguaggio di programmazione completo di Turing. Questo si chiama "equivalenza di Turing" e significa solo esattamente ciò che dice. È importante sottolineare che non significa che puoi fare l'altra cosa altrettanto facilmente nell'altra lingua - alcuni potrebbero sostenere che questo è l'intero punto della creazione di un nuovo linguaggio di programmazione: per darti un modo migliore di fare certe cose che fanno schifo le lingue esistenti.

Un sistema di tipo dinamico, ad esempio, può essere emulato sopra un sistema di tipo OO statico dichiarando semplicemente tutte le variabili, i parametri e i valori di ritorno come Objecttipo di base e quindi usando la riflessione per accedere ai dati specifici all'interno, quindi quando ti rendi conto di questo vedi che non c'è letteralmente niente che puoi fare in un linguaggio dinamico che non puoi fare in un linguaggio statico. Ma farlo in questo modo sarebbe un gran casino, ovviamente.

Il ragazzo della citazione ha ragione nel dire che i tipi statici limitano ciò che puoi fare, ma questa è una caratteristica importante, non un problema. Le linee sulla strada limitano ciò che puoi fare nella tua auto, ma le trovi restrittive o utili? (So ​​che non vorrei guidare su una strada trafficata e complessa in cui non c'è niente che dice alle macchine che vanno nella direzione opposta per tenersi dalla loro parte e non venire dove sto guidando!) Stabilendo regole che delineano chiaramente ciò che è considerato un comportamento non valido e assicurando che non accada, si riducono notevolmente le possibilità che si verifichi un brutto incidente.

Inoltre, sta caratterizzando male l'altra parte. Non è che "tutti i programmi interessanti che vuoi scrivere funzioneranno come tipi", ma piuttosto "tutti i programmi interessanti che vuoi scrivere richiederanno tipi". Una volta superato un certo livello di complessità, diventa molto difficile mantenere la base di codice senza un sistema di tipi per mantenerti in linea, per due motivi.

Innanzitutto, perché il codice senza annotazioni di tipo è difficile da leggere. Considera il seguente Python:

def sendData(self, value):
   self.connection.send(serialize(value.someProperty))

Come prevedete che i dati assomiglino al sistema ricevuto dall'altra parte della connessione? E se sta ricevendo qualcosa che sembra completamente sbagliato, come fai a capire cosa sta succedendo?

Tutto dipende dalla struttura di value.someProperty. Ma che aspetto ha? Buona domanda! Cosa sta chiamando sendData()? Che cosa sta passando? Che aspetto ha quella variabile? Da dove proviene? Se non è locale, è necessario rintracciare l'intera storia valueper rintracciare ciò che sta accadendo. Forse stai passando qualcos'altro che ha anche una somePropertyproprietà, ma non fa quello che pensi che faccia?

Ora diamo un'occhiata con le annotazioni dei tipi, come potresti vedere nel linguaggio Boo, che utilizza una sintassi molto simile ma è tipicamente statico:

def SendData(value as MyDataType):
   self.Connection.Send(Serialize(value.SomeProperty))

Se c'è qualcosa che non va, improvvisamente il tuo lavoro di debug ha appena ottenuto un ordine di grandezza più semplice: cerca la definizione di MyDataType! Inoltre, la possibilità di ottenere comportamenti scorretti perché hai passato un tipo incompatibile che ha anche una proprietà con lo stesso nome improvvisamente va a zero, perché il sistema dei tipi non ti consente di commettere quell'errore.

Il secondo motivo si basa sul primo: in un progetto ampio e complesso, molto probabilmente hai più collaboratori. (E se no, lo stai costruendo da te per molto tempo, che è essenzialmente la stessa cosa. Prova a leggere il codice che hai scritto 3 anni fa se non mi credi!) Ciò significa che non sai cosa fosse passare attraverso il capo della persona che ha scritto quasi ogni parte del codice nel momento in cui l'hanno scritto, perché non eri lì, o non ricordi se era il tuo codice molto tempo fa. Avere dichiarazioni di tipo ti aiuta davvero a capire qual era l'intenzione del codice!

Le persone come il ragazzo nella citazione spesso descrivono erroneamente i vantaggi della tipizzazione statica come "aiutare il compilatore" o "tutto sull'efficienza" in un mondo in cui risorse hardware quasi illimitate lo rendono sempre meno rilevante ogni anno che passa. Ma come ho dimostrato, mentre certamente esistono questi benefici, il vantaggio principale risiede nei fattori umani, in particolare la leggibilità e la manutenibilità del codice. (L'efficienza aggiunta è certamente un bel bonus, però!)


24
"Questo ragazzo è un troll." - Non sono sicuro che un attacco ad hominem possa aiutare il tuo caso altrimenti ben presentato. E mentre sono ben consapevole che l'argomentazione dell'autorità è un errore altrettanto grave come ad hominem, vorrei comunque sottolineare che Gilad Bracha ha probabilmente progettato più lingue e (più rilevante per questa discussione) più sistemi di tipo statico rispetto alla maggior parte. Solo un piccolo estratto: è l'unico designer di Newspeak, co-designer di Dart, co-autore di Java Language Specification e Java Virtual Machine Specification, ha lavorato alla progettazione di Java e JVM, progettato ...
Jörg W Mittag

10
Strongtalk (un sistema di tipo statico per Smalltalk), il sistema di tipo Dart, il sistema di tipo Newspeak, la sua tesi di dottorato sulla modularità è alla base di quasi tutti i sistemi di moduli moderni (ad esempio Java 9, ECMAScript 2015, Scala, Dart, Newspeak, Ioke , Seph), i suoi articoli sui mixin hanno rivoluzionato il modo in cui pensiamo a loro. Ora, ciò non vuol dire che ha ragione, ma mi fanno pensare che avere progettati più sistemi di tipo statico lo rende un po 'più di un "troll".
Jörg W Mittag,

17
"Sebbene sia vero che i sistemi di tipi" ti limitano a un sottoinsieme ", per definizione, le cose esterne a quel sottoinsieme sono cose che non funzionano". - Questo è sbagliato. Sappiamo dall'indecidibilità del problema dell'arresto, dal teorema di Rice e dalla miriade di altri risultati indecidibili e non calcolabili che un controllo statico del tipo non può decidere per tutti i programmi se sono sicuri o non sicuri. Non può accettare quei programmi (alcuni dei quali non sono sicuri per il tipo), quindi l'unica scelta sana è quella di rifiutarli (tuttavia, alcuni di questi sono sicuri per il tipo). In alternativa, la lingua deve essere progettata in ...
Jörg W Mittag

9
... in modo tale da rendere impossibile per il programmatore la scrittura di quei programmi indecidibili, ma ancora una volta, alcuni di questi sono effettivamente sicuri per i tipi. Quindi, non importa come lo tagli: al programmatore viene impedito di scrivere programmi sicuri. E dal momento che ci sono in realtà infinitamente molti di loro (di solito), possiamo essere quasi certi che almeno alcuni di loro non sono solo roba che fa il lavoro, ma anche utile.
Jörg W Mittag,

8
@MasonWheeler: il problema Halting si presenta continuamente, proprio nel contesto del controllo statico del tipo. A meno che le lingue non siano progettate con cura per impedire al programmatore di scrivere determinati tipi di programmi, il controllo statico dei tipi diventa rapidamente equivalente alla risoluzione del problema di interruzione. O finisci con programmi che non ti è permesso scrivere perché potrebbero confondere il controllo del tipo, o finisci con programmi che ti è permesso scrivere ma impiegano un tempo infinito per scrivere il controllo.
Jörg W Mittag,

27

Ho intenzione di fare un passo avanti nella parte "modello" perché penso che si trasformi nella definizione di ciò che è o non è un modello e ho perso da tempo l'interesse per quel dibattito. Quello che dirò è che ci sono cose che puoi fare in alcune lingue che non puoi fare in altre. Sia chiaro, io non dicendo che ci sono problemi si possono risolvere in una lingua che non è possibile risolvere in un altro. Mason ha già sottolineato la completezza di Turing.

Ad esempio, ho scritto una classe in Python che accetta un elemento DOM XML e lo trasforma in un oggetto di prima classe. Cioè, puoi scrivere il codice:

doc.header.status.text()

e hai il contenuto di quel percorso da un oggetto XML analizzato. un po 'pulito e ordinato, IMO. E se non esiste un nodo head, restituisce solo oggetti fittizi che contengono nient'altro che oggetti fittizi (tartarughe fino in fondo.) Non c'è modo reale di farlo in Java, per esempio. Dovresti aver compilato una classe in anticipo che si basasse su una certa conoscenza della struttura dell'XML. Mettendo da parte se questa è una buona idea, questo tipo di cose cambia davvero il modo in cui risolvi i problemi in un linguaggio dinamico. Non sto dicendo che cambi in un modo che è necessariamente sempre migliore, comunque. Ci sono alcuni costi definiti per gli approcci dinamici e la risposta di Mason offre una panoramica decente. Se sono una buona scelta dipende da molti fattori.

In una nota a margine, puoi farlo in Java perché puoi costruire un interprete python in Java . Il fatto che risolvere un problema specifico in una determinata lingua possa significare costruire un interprete o qualcosa di simile ad esso è spesso trascurato quando le persone parlano della completezza di Turing.


4
Non puoi farlo in Java perché Java è mal progettato. Non sarebbe stato così difficile usare C # IDynamicMetaObjectProvider, ed è morto in Boo. ( Ecco un'implementazione in meno di 100 righe, inclusa come parte dell'albero dei sorgenti standard su GitHub, perché è così facile!)
Mason Wheeler

6
@MasonWheeler "IDynamicMetaObjectProvider"? È legato alla dynamicparola chiave di C # ? ... che in effetti si limita a digitare in modo dinamico su C #? Non sono sicuro che il tuo argomento sia valido se ho ragione.
jpmc26,

9
@MasonWheeler Stai entrando in semantica. Senza entrare in un dibattito sulle minuzie (non stiamo sviluppando un formalismo matematico su SE qui.), La tipizzazione dinamica è la pratica di rinunciare alle decisioni sui tempi di compilazione intorno ai tipi, in particolare la verifica che ogni tipo abbia i membri particolari a cui il programma accede. Questo è l'obiettivo che dynamicraggiunge in C #. Le "ricerche di riflessione e di dizionario" avvengono in fase di esecuzione, non durante la compilazione. Non sono davvero sicuro di come si possa sostenere che non aggiunge la digitazione dinamica alla lingua. Il mio punto è che l'ultimo paragrafo di Jimmy lo copre.
jpmc26,

44
Pur non essendo un grande fan di Java, oserei anche dire che chiamare Java "mal progettato" in particolare perché non ha aggiunto la digitazione dinamica è ... troppo zelante.
jpmc26,

5
A parte la sintassi leggermente più comoda, in cosa differisce da un dizionario?
Theodoros Chatzigiannakis,

10

La citazione è corretta, ma anche davvero disingenua. Analizziamolo per vedere perché:

La cosa meravigliosa della digitazione dinamica è che ti consente di esprimere tutto ciò che è calcolabile.

Bene, non proprio. Un linguaggio con la digitazione dinamica ti consente di esprimere qualsiasi cosa purché sia Turing completo , che la maggior parte lo è. Lo stesso sistema di tipi non ti consente di esprimere tutto. Dagli però il beneficio del dubbio qui.

E i sistemi di tipo no: i sistemi di tipo sono in genere decidibili e ti limitano a un sottoinsieme.

Questo è vero, ma notiamo ora che stiamo parlando fermamente di ciò che consente il sistema di tipi , non di ciò che consente il linguaggio che utilizza un sistema di tipi. Mentre è possibile utilizzare un sistema di tipi per calcolare roba in fase di compilazione, questo in genere non è Turing completo (poiché il sistema di tipi è generalmente decidibile), ma quasi ogni linguaggio tipicamente statico è anche Turing completo nel suo runtime (le lingue tipicamente dipendenti sono no, ma non credo che ne stiamo parlando qui).

Le persone che preferiscono i sistemi di tipo statico dicono "va bene, è abbastanza buono; tutti i programmi interessanti che vuoi scrivere funzioneranno come tipi ”. Ma è ridicolo: una volta che hai un sistema di tipi, non sai nemmeno quali programmi interessanti ci sono.

Il problema è che i tipi dinamici delle lingue hanno un tipo statico. A volte tutto è una stringa, e più comunemente c'è qualche unione taggata in cui ogni cosa è una borsa di proprietà o un valore come un int o un doppio. Il problema è che anche i linguaggi statici possono fare questo, storicamente è stato un po 'più complicato farlo, ma i moderni linguaggi tipicamente statici lo rendono abbastanza facile come usare un linguaggio di tipi dinamici, quindi come può esserci una differenza in cosa può vedere il programmatore come un programma interessante? Le lingue statiche hanno esattamente gli stessi sindacati con tag e altri tipi.

Per rispondere alla domanda nel titolo: No, non ci sono schemi di progettazione che non possono essere implementati in un linguaggio tipicamente statico, perché puoi sempre implementare abbastanza di un sistema dinamico per ottenerli. Potrebbero esserci dei pattern che ottieni "gratis" in un linguaggio dinamico; questo può o non può valere la pena sopportare gli aspetti negativi di tali lingue per YMMV .


2
Non sono del tutto sicuro se hai appena risposto sì o no. Mi sembra più un no.
user7610

1
@TheodorosChatzigiannakis Sì, come potrebbero essere implementati i linguaggi dinamici? In primo luogo, passerai per un architetto astronauta se mai vuoi implementare un sistema di classe dinamica o qualcos'altro un po 'coinvolto. In secondo luogo, probabilmente non hai le risorse per renderlo debuggable, completamente introspettibile, performante ("usa solo un dizionario" è come vengono implementate le lingue lente). In terzo luogo, alcune funzioni dinamiche sono meglio utilizzate quando vengono integrate in tutto il linguaggio, non solo come una libreria: pensate ad esempio alla garbage collection (ci sono GC come librerie, ma non sono comunemente usati).
coredump,

1
@Theodoros Secondo l'articolo che ho già collegato qui una volta, tutte tranne il 2,5% delle strutture (nei moduli Python esaminati dalle ricerche) possono essere facilmente espresse in un linguaggio tipizzato. Forse il 2,5% vale la pena pagare i costi della digitazione dinamica. Questo è essenzialmente quello che la mia domanda riguardava. neverworkintheory.org/2016/06/13/polymorphism-in-python.html
user7610

3
@JiriDanek Per quanto ne so, non c'è nulla che impedisca a un linguaggio tipicamente statico di avere punti di chiamata polimorfici e mantenere la tipizzazione statica nel processo. Vedere Verifica del tipo statico di metodi multipli . Forse sto fraintendendo il tuo link.
Theodoros Chatzigiannakis,

1
"Un linguaggio con la digitazione dinamica ti consente di esprimere qualsiasi cosa purché sia ​​Turing completo, che è la maggior parte." Mentre questa è ovviamente una vera affermazione, non tiene davvero nel "mondo reale" perché la quantità di testo che uno ha scrivere potrebbe essere estremamente grande.
Daniel Jour,

4

Ci sono sicuramente cose che puoi fare solo in lingue tipizzate dinamicamente. Ma non sarebbero necessariamente un buon design.

È possibile assegnare prima un numero intero 5, quindi una stringa 'five'o un Catoggetto alla stessa variabile. Ma stai solo rendendo più difficile per un lettore del tuo codice capire cosa sta succedendo, qual è lo scopo di ogni variabile.

È possibile aggiungere un nuovo metodo a una libreria Ruby class e accedere ai suoi campi privati. Potrebbero esserci casi in cui un tale hack può essere utile, ma ciò costituirebbe una violazione dell'incapsulamento. (Non mi dispiace aggiungere metodi basati solo sull'interfaccia pubblica, ma non è nulla che i metodi di estensione C # digitati staticamente non possano fare.)

È possibile aggiungere un nuovo campo a un oggetto della classe di qualcun altro per passare alcuni dati extra con esso. Ma è meglio progettare semplicemente creare una nuova struttura o estendere il tipo originale.

In generale, più organizzato si desidera che il codice rimanga, meno vantaggio si dovrebbe trarre dalla possibilità di modificare dinamicamente le definizioni dei tipi o assegnare valori di tipi diversi alla stessa variabile. Ma poi il tuo codice non è diverso da quello che potresti ottenere in un linguaggio tipicamente statico.

In quali lingue dinamiche sono bravi lo zucchero sintattico. Ad esempio, quando si legge un oggetto JSON deserializzato, è possibile fare riferimento a un valore nidificato semplicemente come obj.data.article[0].content- molto più ordinato di quanto si pensi obj.getJSONObject("data").getJSONArray("article").getJSONObject(0).getString("content").

Gli sviluppatori di Ruby in particolare potrebbero parlare a lungo della magia che può essere raggiunta implementando method_missing, che è un metodo che consente di gestire le chiamate tentate a metodi non dichiarati. Ad esempio, ActiveRecord ORM lo utilizza in modo da poter effettuare una chiamata User.find_by_email('joe@example.com')senza mai dichiarare il find_by_emailmetodo. Ovviamente non è nulla che non possa essere raggiunto come UserRepository.FindBy("email", "joe@example.com")in un linguaggio tipicamente statico, ma non si può negare la sua pulizia.


4
Ci sono sicuramente cose che puoi fare solo in lingue tipicamente statiche. Ma non sarebbero necessariamente un buon design.
coredump,

2
Il punto sullo zucchero sintattico ha ben poco a che fare con la tipizzazione dinamica e tutto con, beh, la sintassi.
sinistra il

@leftaroundabout I pattern hanno tutto a che fare con la sintassi. Anche i sistemi di tipo hanno molto a che fare con questo.
user253751

4

Il modello di proxy dinamico è un collegamento per l'implementazione di oggetti proxy senza la necessità di una classe per tipo che è necessario eseguire il proxy.

class Proxy(object):
    def __init__(self, obj):
        self.__target = obj

    def __getattr__(self, attr):
        return getattr(self.__target, attr)

Usando questo, Proxy(someObject)crea un nuovo oggetto che si comporta come someObject. Ovviamente vorrai anche aggiungere funzionalità aggiuntive in qualche modo, ma questa è una base utile da cui iniziare. In un linguaggio statico completo, dovresti scrivere una classe Proxy per tipo che desideri proxy o utilizzare la generazione di codice dinamico (che, è vero, è inclusa nella libreria standard di molti linguaggi statici, soprattutto perché i loro progettisti sono a conoscenza i problemi non riuscire a fare questa causa).

Un altro caso d'uso di linguaggi dinamici è il cosiddetto "patching delle scimmie". In molti modi, questo è un anti-pattern piuttosto che un pattern, ma può essere usato in modi utili se fatto con cura. E mentre non esiste alcun motivo teorico per cui il patching delle scimmie non possa essere implementato in un linguaggio statico, non ne ho mai visto uno che lo abbia effettivamente.


Penso che potrei essere in grado di emularlo in Go. C'è una serie di metodi che tutti gli oggetti proxy devono avere (altrimenti l'anatra potrebbe non cedere e tutto cade a pezzi). Posso creare un'interfaccia Go con questi metodi. Dovrò pensarci di più, ma penso che ciò che ho in mente funzionerà.
user7610

Puoi realizzare qualcosa di simile in qualsiasi linguaggio .NET con RealProxy e generics.
LittleEwok,

@LittleEwok - RealProxy utilizza la generazione del codice di runtime - come ho detto, molti linguaggi statici moderni hanno una soluzione come questa, ma è ancora più facile in un linguaggio dinamico.
Jules il

I metodi di estensione C # sono un po 'come il patching delle scimmie reso sicuro. Non è possibile modificare i metodi esistenti, ma è possibile aggiungerne di nuovi.
Andrew dice Reinstate Monica il

3

, ci sono molti modelli e tecniche che sono possibili solo in un linguaggio tipizzato dinamicamente.

Il patching delle scimmie è una tecnica in cui proprietà o metodi vengono aggiunti a oggetti o classi in fase di esecuzione. Questa tecnica non è possibile in un linguaggio tipicamente statico poiché ciò significa che i tipi e le operazioni non possono essere verificati al momento della compilazione. O per dirla in altro modo, se un linguaggio supporta il patching delle scimmie è per definizione un linguaggio dinamico.

È possibile dimostrare che se un linguaggio supporta il patching delle scimmie (o tecniche simili per modificare i tipi in fase di esecuzione), non può essere controllato staticamente. Quindi non è solo una limitazione nelle lingue attualmente esistenti, è una limitazione fondamentale della tipizzazione statica.

Quindi la citazione è decisamente corretta: più cose sono possibili in un linguaggio dinamico che in un linguaggio tipicamente statico. D'altra parte, alcuni tipi di analisi sono possibili solo in un linguaggio tipicamente statico. Ad esempio, sai sempre quali operazioni sono consentite su un determinato tipo, il che ti consente di rilevare operazioni illegali al tipo di compilazione. Tale verifica non è possibile in un linguaggio dinamico quando le operazioni possono essere aggiunte o rimosse in fase di esecuzione.

Questo è il motivo per cui non esiste un "migliore" ovvio nel conflitto tra linguaggi statici e dinamici. I linguaggi statici rinunciano a una certa potenza in fase di esecuzione in cambio di un diverso tipo di potenza in fase di compilazione, che a loro avviso riduce il numero di bug e facilita lo sviluppo. Alcuni credono che ne sia valsa la pena, altri no.

Altre risposte hanno sostenuto che l'equivalenza di Turing significa che tutto il possibile in una lingua è possibile in tutte le lingue. Ma questo non segue. Per supportare qualcosa come il patching delle scimmie in un linguaggio statico, in pratica devi implementare un sotto-linguaggio dinamico all'interno del linguaggio statico. Questo è ovviamente possibile, ma direi che stai quindi programmando in un linguaggio dinamico incorporato, poiché perdi anche il controllo statico del tipo che esiste nella lingua host.

C # dalla versione 4 supporta oggetti digitati dinamicamente. Chiaramente i progettisti linguistici vedono il vantaggio di avere entrambi i tipi di digitazione disponibili. Ma mostra anche che non puoi avere la tua torta e mangiare anche io: quando usi oggetti dinamici in C # ottieni l'abilità di fare qualcosa come il patching delle scimmie, ma perdi anche la verifica del tipo statico per l'interazione con questi oggetti.


+1 il tuo penultimo paragrafo penso sia l'argomento cruciale. Continuerei a sostenere che esiste una differenza, tuttavia, come per i tipi statici, hai il pieno controllo di dove e cosa puoi applicare a patch
jk.

2

Mi chiedo, ci sono utili modelli o strategie di progettazione che, usando la formulazione della citazione, "non funzionano come tipi"?

Sì e no.

Ci sono situazioni in cui il programmatore conosce il tipo di una variabile con maggiore precisione rispetto a un compilatore. Il compilatore può sapere che qualcosa è un oggetto, ma il programmatore saprà (a causa degli invarianti del programma) che in realtà è una stringa.

Lasciami mostrare alcuni esempi di questo:

Map<Class<?>, Function<?, String>> someMap;
someMap.get(object.getClass()).apply(object);

So che someMap.get(T.class)restituirà a Function<T, String>, a causa di come ho creato someMap. Ma Java è solo sicuro di avere una funzione.

Un altro esempio:

data = parseJSON(someJson)
validate(data, someJsonSchema);
print(data.properties.rowCount);

So che data.properties.rowCount sarà un riferimento valido e un numero intero, perché ho convalidato i dati su uno schema. Se quel campo mancasse, sarebbe stata generata un'eccezione. Ma un compilatore saprebbe solo che sta generando un'eccezione o restituisce una sorta di JSONValue generico.

Un altro esempio:

x, y, z = struct.unpack("II6s", data)

"II6s" definisce il modo in cui i dati codificano tre variabili. Da quando ho specificato il formato, so quali tipi verranno restituiti. Un compilatore saprebbe solo che restituisce una tupla.

Il tema unificante di tutti questi esempi è che il programmatore conosce il tipo, ma un sistema di tipo Java non sarà in grado di riflettere questo. Il compilatore non conoscerà i tipi e quindi un linguaggio tipizzato staticamente non mi permetterà di chiamarli, mentre un linguaggio tipizzato in modo dinamico lo farà.

Ecco a cosa sta arrivando la citazione originale:

La cosa meravigliosa della digitazione dinamica è che ti consente di esprimere tutto ciò che è calcolabile. E i sistemi di tipo no: i sistemi di tipo sono in genere decidibili e ti limitano a un sottoinsieme.

Quando uso la tipizzazione dinamica posso usare il tipo più derivato che conosco, non semplicemente il tipo più derivato che il sistema di tipi della mia lingua conosce. In tutti i casi precedenti, ho un codice che è semanticamente corretto, ma sarà rifiutato da un sistema di tipizzazione statico.

Tuttavia, per tornare alla tua domanda:

Mi chiedo, ci sono utili modelli o strategie di progettazione che, usando la formulazione della citazione, "non funzionano come tipi"?

Uno qualsiasi degli esempi precedenti, e in effetti qualsiasi esempio di tipizzazione dinamica, può essere reso valido nella tipizzazione statica aggiungendo i cast appropriati. Se conosci un tipo che il tuo compilatore non ha, basta dirlo al compilatore lanciando il valore. Quindi, ad un certo livello, non otterrai alcun pattern aggiuntivo usando la digitazione dinamica. Potrebbe essere necessario eseguire il cast di più per far funzionare il codice digitato staticamente.

Il vantaggio della digitazione dinamica è che puoi semplicemente usare questi schemi senza preoccuparti del fatto che è difficile convincere il tuo sistema di tipi della loro validità. Non cambia i modelli disponibili, ma li rende probabilmente più facili da implementare perché non devi capire come far riconoscere il modello al tuo sistema di tipi o aggiungere cast per sovvertire il sistema di tipi.


1
perché java è il punto di interruzione in cui non dovresti andare a un "sistema di tipo più avanzato / complicato"?
jk.

2
@jk, cosa ti porta a pensare che è quello che sto dicendo? Ho evitato esplicitamente di schierarmi sulla validità o meno di un sistema di tipo più avanzato / complicato.
Winston Ewert,

2
Alcuni di questi sono esempi terribili e gli altri sembrano essere più decisioni linguistiche piuttosto che tipizzate vs non tipizzate. Sono particolarmente confuso sul perché le persone pensano che la deserializzazione sia così complessa nei linguaggi tipizzati. Il risultato tipizzato sarebbe data = parseJSON<SomeSchema>(someJson); print(data.properties.rowCount); e se uno non ha una classe a cui deserializzare possiamo tornare indietro data = parseJSON(someJson); print(data["properties.rowCount"]);- che è ancora digitato ed esprime lo stesso intento.
NPSF3000,

2
@ NPSF3000, come funziona la funzione parseJSON? Sembrerebbe usare sia la riflessione che le macro. Come è possibile digitare i dati ["properties.rowCount"] in un linguaggio statico? Come potrebbe sapere che il valore risultante è un numero intero?
Winston Ewert,

2
@ NPSF3000, come pensi di usarlo se non sai che è un numero intero? Come pensi di passare in rassegna gli elementi in un elenco in JSON senza sapere che si trattava di un array? Il punto del mio esempio era che sapevo che data.propertiesera un oggetto e sapevo che data.properties.rowCountera un numero intero e potevo semplicemente scrivere il codice che li usava. La tua proposta data["properties.rowCount"]non fornisce la stessa cosa.
Winston Ewert,

1

Ecco alcuni esempi di Objective-C (tipizzati in modo dinamico) che non sono possibili in C ++ (digitati staticamente):

  • Mettere oggetti di più classi distinte nello stesso contenitore.
    Naturalmente, ciò richiede l'ispezione del tipo di runtime per interpretare successivamente il contenuto del contenitore e la maggior parte degli amici di tipizzazione statica obietterà che non dovresti farlo in primo luogo. Ma ho scoperto che, al di là dei dibattiti religiosi, questo può tornare utile.

  • Espandere una classe senza sottoclasse.
    In Objective-C, puoi definire nuove funzioni membro per le classi esistenti, comprese quelle definite dal linguaggio come NSString. Ad esempio, è possibile aggiungere un metodo stripPrefixIfPresent:, in modo da poterlo dire [@"foo/bar/baz" stripPrefixIfPresent:@"foo/"](notare l'uso dei NSSringletterali @"").

  • Utilizzo di callback orientati agli oggetti.
    In linguaggi tipicamente statici come Java e C ++, è necessario fare di tutto per consentire a una libreria di chiamare un membro arbitrario di un oggetto fornito dall'utente. In Java, la soluzione alternativa è la coppia interfaccia / adattatore più una classe anonima, in C ++ la soluzione alternativa è in genere basata su un modello, il che implica che il codice della libreria deve essere esposto al codice utente. In Objective-C, è sufficiente passare il riferimento all'oggetto più il selettore per il metodo alla libreria e la libreria può richiamare semplicemente e direttamente il callback.


Posso fare il primo in C ++ lanciando a void *, ma questo sta aggirando il sistema dei tipi, quindi non conta. Posso fare il secondo in C # con metodi di estensione, perfettamente all'interno di un sistema di tipi. Per il terzo, penso che il "selettore per il metodo" possa essere un lambda, quindi qualsiasi linguaggio tipizzato staticamente con lambdas può fare lo stesso, se capisco correttamente. Non ho familiarità con ObjC.
user7610

1
@JiriDanek "Posso fare il primo in C ++ eseguendo il casting per annullare *", non esattamente, il codice che legge gli elementi non ha modo di recuperare il tipo reale da solo. Hai bisogno di tag di tipo. Inoltre, non penso che dire "Posso farlo in <language>" sia il modo appropriato / produttivo di vederlo, perché puoi sempre emularli. Ciò che conta è il guadagno in espressività rispetto alla complessità dell'implementazione. Inoltre, sembra che tu abbia una capacità sia statica che dinamica (Java, C #), che appartiene esclusivamente alla famiglia di lingue "statiche".
coredump,

1
@JiriDanek da void*solo non è una digitazione dinamica, è una mancanza di battitura. Ma sì, dynamic_cast, tabelle virtuali ecc. Rendono C ++ non tipicamente statico. È male?
coredump

1
Suggerisce che avere la possibilità di sovvertire il sistema di tipi quando necessario è utile. Avere una botola di fuga quando ne hai bisogno. O qualcuno lo ha ritenuto utile. Altrimenti non lo metterebbero nella lingua.
user7610

2
@JiriDanek Penso che tu l'abbia praticamente inchiodato con il tuo ultimo commento. Quei boccaporti di fuga possono essere estremamente utili se usati con cura. Tuttavia, con un grande potere derivano grandi responsabilità, e molte sono le persone che la abusano ... Quindi, è molto meglio usare un puntatore a una classe di base generica da cui tutte le altre classi sono derivate dalla definizione (come nel caso sia in Objective-C che in Java) e fare affidamento su RTTI per distinguere i casi, piuttosto che trasmettere un void*tipo di oggetto specifico. Il primo produce un errore di runtime se hai incasinato, il successivo si traduce in un comportamento indefinito.
cmaster
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.