Design del linguaggio: saltando le occorrenze di un identificatore invece di accedere all'ambito che racchiude

3

Non ho idea di come scrivere un buon titolo per questa domanda.

Sto pensando di introdurre un operatore in una DSL che rende accessibili gli identificatori nascosti (come le variabili). Pensa a this.foo in Java o C # per accedere a un membro nascosto da un foo locale. O base.bar quando bar è un membro nella classe corrente. Chiamiamo questo "accesso esplicito a un ambito che racchiude".

I destinatari del DSL sono non programmatori / principianti, che non dovrebbero essere richiesti per comprendere (in dettaglio) gli ambiti in cui stanno lavorando. Quindi l'idea è di introdurre invece un operatore che salta un'occorrenza di un identificatore e può essere applicato più volte. Il compilatore farà la sua solita cosa quando risolverà gli identificatori, iniziando dall'ambito locale e andando verso l'esterno. Quando trova qualcosa e l'operatore viene utilizzato, ignora il risultato e continua a cercare.

Considera questo esempio di pseudo-codice:

foo = 1 // global
object bar {
  foo = "member foo" 
  function bla() {
    let foo = 2
    // _ we are here
  }
}

Nella posizione di destinazione, foo fa riferimento alla variabile locale con valore 2 .
@foo sarà il membro di tipo stringa (salta un'occorrenza)
@@foo sarà il globale variabile con valore 1 (saltare due)

Le alternative in linguaggi come Java o C # sarebbero this.foo per il membro e qualche immaginario global::foo per l'immaginario globale. Il problema è che a) gli utenti devono imparare diverse parole chiave eb) gli utenti devono comprendere la struttura esatta degli ambiti (cosa fa this fa riferimento a ecc.)

La vera domanda: è una cattiva idea, ci sono alcuni problemi gravi che non vedo?
O ci sono forse delle lingue che impiegano un operatore del genere che potrei esaminare?

Un po 'più di contesto che potrebbe essere rilevante:

  • questo non è un linguaggio di programmazione generico
  • è un mix di elementi testuali e visivi (pensa Excel), quindi è chiaro visivamente cosa è contenuto in cosa (e dove ci porteranno le occorrenze)
  • tipizzazione statica, nessun ambito dinamico (valutazione in fase di compilazione)
  • L'idea è che le persone "capiscano" che la loro cosa abbia lo stesso nome di una cosa diversa, ma non conoscono le parole magiche per riferirsi esplicitamente al contenitore di quella cosa.
  • scrivendo @foo dove c'è solo un foo provocherà un errore

Aggiornamento: alcune osservazioni:

  • Ambiti lessicali: ho omesso di menzionarlo, ma l'operatore dovrebbe sfuggire a tutti gli ambiti lessicali quando viene utilizzato per primo. Se ci sono 3 variabili in 3 blocchi annidati, come loop o funzioni, non puoi usare @ per affrontarli. È inteso solo per accedere a scope non lessicali (oggetti, ambito globale). Credo che gli ambiti lessicali siano ancora più difficili da comprendere per i principianti e quindi non capirebbero quanti% di% di co_de avrebbero bisogno di rivolgersi a un membro.
  • Fragilità: come menziona John R. Strohm, l'introduzione di un nuovo @ o la rimozione di uno da un oggetto interromperanno tutti i riferimenti a% esistentefoo resp. il numero di foo richiesto per indirizzarli cambierà. Questo è davvero un problema, ma non è questo il caso, ad es. C # pure? Considera questo esempio:

.

namespace foo {
    class X {
        public static int Value;
    }

    namespace bar {
        class foo { }
        class X {}

        class Main {
            public static int Value = global::foo.X.Value + 1;
        }
    }
}

Creando un nuovo @ gli utenti possono nascondere altri tipi e spazi dei nomi negli ambiti esterni. Il codice esistente si interromperà e dovrà essere aggiornato, anteponendo i nomi dei nomi dei nomi o class se i namespace fossero nascosti.

Il punto è: questo problema esiste in linguaggi come C # e non causa problemi. Il problema è più grave con l'operatore global:: , perché la rimozione delle cose causerà anche il fallimento, mentre l'indirizzamento esplicito degli ambiti risolve il problema una volta per tutte. La mia argomentazione a riguardo è questa: quando devi aggiustare il codice quando aggiungi roba, ci si può aspettare che tu debba aggiustarlo anche quando rimuovi qualcosa.

Aggiornamento: Ho accettato la risposta di John perché sottolinea un problema che ritengo sia peggiore di quello che molti altri hanno menzionato (copiare il codice in una posizione diversa può alterare quali simboli vengono indirizzati). < br> Il problema è che se un utente crea un nuovo simbolo @ in un livello, modifica i simboli foo risolti in livelli nidificati. In altre parole, una modifica invisibile al codice che non è attualmente in corso di modifica. A mio avviso questo è peggio del problema del copia-incolla, in quanto almeno può essere trattato in quel preciso istante e mentre continua a passare inosservato, l'utente ha una maggiore possibilità di scoprirlo.

    
posta enzi 18.07.2017 - 18:33
fonte

6 risposte

1

Hai affermato che intendi lo scope lessicale anziché lo scope dinamico. Mentre questo risolve alcuni problemi, ne crea altri. (GNU Emacs LISP ha un ambito dinamico, in modo da consentire agli utenti di scrivere estensioni che modificano il comportamento di altre estensioni.)

Sembra che tu intenda consentire più livelli di scoping, potenzialmente richiedendo all'utente di fare qualcosa come "@@@@@@@", per salire di una mezza dozzina di livelli per arrivare a qualcosa, se "foo" "si rivela essere un identificatore molto popolare.

L'ovvio guastafeste è questo: se hai un "foo" tre livelli in alto, questo non è nascosto da nessuna parte tra lì e qui, e puoi arrivarci semplicemente dicendo "pippo", quindi ci sono almeno tre luoghi in cui qualcun altro può violare il codice semplicemente definendo un nuovo "pippo".

Questa mi sembra una ricetta per il disastro.

Suggerirei invece di prendere in considerazione l'ipotesi di abbandonare il modello di procedura nidificato e di passare invece a un modello "piatto". Hai variabili "locali", e poi hai importato oggetti, chiamati per nome, dì "foo", e le entità all'interno di quegli oggetti usano una sintassi qualificata, diciamo "foo.waldo". (Sì, è possibile utilizzare la sintassi del puntatore C / C ++, "foo- > waldo" o anche la sintassi del puntatore PASCAL "foo ^ .waldo".)

    
risposta data 19.07.2017 - 00:04
fonte
4

A quanto ho capito, stai suggerendo un costrutto: @ prefisso a un identificatore, ad es. @foo , che va al prossimo (2 °) visibile foo a partire dall'ambito corrente passando agli ambiti esterni.

Quindi, scrivere @foo richiede l'esistenza di un 2% difoo s nell'ambito (nascondendo l'altro).

Non mi interessa il design, perché significa che il codice scritto usando quel costrutto, ad es. @foo , cattura con esso la conferma della presenza di un altro foo (a cui non è interessato) rendendo il codice più fragile, perché - ad un modo di guardarlo - è un riferimento sia di foo S.

Il codice scritto utilizzando @@foo acquisisce implicitamente l'esistenza di altri due foo s a cui non è interessato.

Questo mi suggerisce che il codice semplice sarà soggetto a più copie & incolla errori.

(Inoltre, un foo e bar nello stesso ambito potrebbero richiedere @ per uno ma non l'altro - preferirei entrambi o nessuno, come negli esempi che hai fornito, re: this. e global:: .)

    
risposta data 18.07.2017 - 20:33
fonte
3

Pensa a quanto è facile commettere un errore. Ad esempio se qualcuno scrive

@@foo = @foo

invece di

@foo = @@foo

Ora, per quanto tempo pensi che i tuoi programmatori inesperti potrebbero individuare questo errore.

Quello che penso avrebbe più senso è quello di rendere più facile trovare quello che ti piace.

Ad esempio

  • @ global.foo (ok capisco questo)
  • bar.foo (questo è il foo definito nell'oggetto bar)
  • bla.foo (questo è il foo definito nella funzione bla)

Ma presumendo che avresti davvero bisogno di accedere agli ambiti periferici. nel mio caso, una DSL dovrebbe ridurre al minimo la complessità. Quindi introdurre strumenti che evitino il bisogno di qualcosa del genere. Un DSL dovrebbe riguardare la scrittura di regole di business nel modo in cui lo comprendono. Se hai nidificato i tuoi ambiti 5 in profondità, sei sulla strada sbagliata.

Aggiornamento: Un altro modo di affrontare questo problema è di non consentire mai variabili globali 'regolari'. Se una variabile globale viene sempre definita e chiamata (esplicitamente) come "global.foo", si evita anche lo scontro nella denominazione tra 2 variabili foo. Un utente dovrebbe definire una variabile come questa

let global.foo = 5

se voleva una variabile globale

    
risposta data 18.07.2017 - 20:33
fonte
1

Tali costrutti linguistici non sono realmente necessari: se un utente vuole accedere a un simbolo in un ambito circostante, potrebbe prima creare un alias per il simbolo esterno nello scope esterno con un nome diverso:

let foo = "outer"
let outer_foo = foo
{
  let foo = "inner"
  // can use foo and outer_foo
}

La maggior parte dei linguaggi di programmazione tradizionali non ha quindi alcun meccanismo per accedere agli ambiti esterni da un ambito nidificato. Correlati ma completamente diversi:

  • accedere ai membri dell'istanza come this.foo in Java, C #; this->foo in C ++; @foo in Ruby.
  • ricerca di un simbolo in uno spazio dei nomi (globale) (ike name.space.Symbol in Java, C #, Python; name::space::Symbol in C ++, Perl.

Un'eccezione degna di nota è Python, dove le parole chiave nonlocal e global possono essere utilizzate per portare una variabile in ambito. Ma questo è necessario perché non è possibile dichiarare una variabile Python: ogni volta che si assegna a una variabile, questa viene creata nell'ambito più interno.

Nelle lingue meno comuni, esistono degli operatori per accedere agli ambiti esterni. Un esempio è il linguaggio del modello Handlebars, che utilizza una sintassi simile al filepath: {{foo}} accede alla variabile nell'ambito corrente, {{../foo}} esamina l'ambito genitore, {{../../foo}} all'ambito secondario e così via. In Perl6, lo pseudo-namespace OUTER ti consente di guardare all'esterno, come in OUTER::Symbol , OUTER::OUTER::Symbol e così via. Ha anche molti altri pseudo-namespace , come OUTERS:: che significa "qualsiasi ambito esterno, non devi contare ".

Questa è una buona idea? Possibilmente. Sebbene non sia strettamente necessario o una buona idea, l'accesso agli ambiti esterni è probabilmente conveniente. Soprattutto se non vuoi spiegare il concetto di "scope lessicale" ai tuoi utenti. Penso, ma non ho prove, che la soluzione pseudo-namespace di Perl6 sia l'approccio più elegante qui per gli ambiti a blocchi. L'utilizzo di un nome esplicito come outer è probabilmente meglio dell'utilizzo di un simbolo dell'oscilloscopio come @ a meno che tu non stia già utilizzando Sigilli in stile Perl o Ruby nella tua lingua.

Tuttavia, questi ambiti esterni espliciti hanno un problema universale: in genere devi conteggiare con attenzione i livelli richiesti, il che rende il codice più difficile da refactoring e copy-paste: se inserisci il codice in un contesto diverso, il livello dell'ambito i conteggi saranno probabilmente spenti.

Per il codice orientato agli oggetti, una parola chiave classica come this , self , my o currentObject potrebbe essere più ragionevole, in cui l'accesso ai simboli avviene come con qualsiasi altro oggetto.

    
risposta data 18.07.2017 - 21:40
fonte
1

Alcuni problemi

  1. In linea di principio, il controllo della portata è una preoccupazione molto importante per i programmatori. Rendere più facile la riproduzione rapida e libera con lo scope probabilmente introdurrà maggiore complessità, non di meno, perché uno scarso controllo dell'ambito rende più difficile l'analisi della dipendenza.

  2. Indicare l'ambito tramite un numero relativo (il numero di simboli @) è soggetto a errori poiché il livellamento dell'ambito può cambiare se, ad esempio, si sposta il codice o se si rimuove un livello. Non sono sicuro di come sarà strutturata la tua lingua, ma i blocchi if e while hanno il loro ambito? Puoi avere classi annidate? Qualsiasi tipo di annidamento risulterebbe in totale confusione, specialmente se dovessi spostare il codice da un blocco all'esterno di quel blocco. Dovresti passare e cambiare ogni @@ in un @.

  3. Puoi finire con due simboli che rappresentano la stessa variabile, ad es. @foo e @@foo potrebbero riferirsi alla stessa locazione di memoria se sono usati in diversi livelli del codice. Questo per me è completamente confuso.

  4. Se i tuoi programmatori non capiscono quale sia l'ambito globale, non c'è un costrutto linguistico nell'universo che possa aiutarli a farlo.

risposta data 19.07.2017 - 03:24
fonte
1

Perl può fare qualcosa di simile a quello che suggerisci ma devi impostarlo per farlo. Questa tecnica funzionerà con entrambe le variabili lessicali e dinamiche.

Uso delle variabili lessicali:

my $foo = 'begin';
my $foo_ref = \$foo;
print "foo=$foo foo_ref=$$foo_ref # both the same at the beginning\n";

{
    my $foo = 'inner';
    print "foo=$foo foo_ref=$$foo_ref # foo changes but foo_ref remains the same\n";

    # magic
    $$foo_ref = 'magic';
    print "foo=$foo foo_ref=$$foo_ref # foo_ref changes but foo is still inner\n";
}
print "foo=$foo foo_ref=$$foo_ref # outer foo changed when foo_ref did\n";

Uso delle variabili dinamiche:

our $foo = 'begin';
our $foo_ref = \$foo;
print "foo=$foo foo_ref=$$foo_ref # both the same at the beginning\n";

{
    local $foo = 'inner';
    print "foo=$foo foo_ref=$$foo_ref # foo changes but foo_ref remains the same\n";

    # magic
    $$foo_ref = 'magic';
    print "foo=$foo foo_ref=$$foo_ref # foo_ref changes but foo is still inner\n";
}
print "foo=$foo foo_ref=$$foo_ref # outer foo changed when foo_ref did\n";
    
risposta data 30.07.2017 - 03:57
fonte