Condividi tramite


Concetti fondamentali

Questa sezione illustra i concetti di base che sono riportati nelle sezioni successive.

Valori

Un singolo elemento di dati è definito valore. In linea di massima, esistono due categorie generali di valori: i valori primitivi, che sono atomici, e i valori strutturati, che vengono costruiti in base a valori primitivi e ad altri valori strutturati. Ad esempio, i valori

1 
true
3.14159 
"abc"

sono primitivi perché non sono costituiti da altri valori. Al contrario, i valori

{1, 2, 3} 
[ A = {1}, B = {2}, C = {3} ]

vengono costruiti usando valori primitivi e, nel caso del record, con altri valori strutturati.

Espressioni

Un'espressione è una formula usata per costruire valori. Può essere formata usando un'ampia gamma di costrutti sintattici. Di seguito sono riportati alcuni esempi di espressioni. Ogni riga corrisponde a un'espressione separata.

"Hello World"             // a text value 
123                       // a number 
1 + 2                     // sum of two numbers 
{1, 2, 3}                 // a list of three numbers 
[ x = 1, y = 2 + 3 ]      // a record containing two fields: 
                          //        x and y 
(x, y) => x + y           // a function that computes a sum 
if 2 > 1 then 2 else 1    // a conditional expression 
let x = 1 + 1  in x * 2   // a let expression 
error "A"                 // error with message "A"

La forma più semplice di espressione, come illustrato sopra, è un valore letterale che rappresenta un valore.

Le espressioni più complesse sono costruite in base ad altre espressioni, denominate sottoespressioni, Ad esempio:

1 + 2

L'espressione precedente è in realtà costituita da tre espressioni. I valori letterali 1 e 2 sono sottoespressioni dell'espressione padre 1 + 2.

L'esecuzione dell'algoritmo definito dai costrutti sintattici usati in un'espressione viene chiamata valutazione dell'espressione. Per ogni tipo di espressione sono definite regole relative al modo in cui questa viene valutata. Ad esempio, un'espressione letterale come 1 produrrà un valore costante, mentre l'espressione a + b userà i valori ottenuti valutando altre due espressioni (a e b) e li unirà in base a un determinato set di regole.

Ambienti e variabili

Le espressioni vengono valutate all'interno di un determinato ambiente. Un ambiente è un set di valori con nome, chiamati variabili. Ogni variabile di un ambiente ha un nome univoco all'interno dell'ambiente denominato identificatore.

Un'espressione di primo livello (o radice) viene valutata all'interno dell'ambiente globale. L'ambiente globale viene fornito dall'analizzatore di espressioni anziché essere determinato in base al contenuto dell'espressione da valutare. Il contenuto dell'ambiente globale include le definizioni della libreria standard e può essere influenzato dalle esportazioni eseguite dalle sezioni di un determinato set di documenti. Per semplicità, negli esempi di questa sezione si presuppone che l'ambiente globale sia vuoto, ovvero che non esista alcuna libreria standard e che non siano presenti altre definizioni basate su sezioni.

L'ambiente usato per valutare una sottoespressione è determinato dall'espressione padre. Quasi tutti i tipi di espressione padre valuteranno una sottoespressione all'interno dello stesso ambiente in cui sono stati valutati, ma alcuni useranno un ambiente diverso. L'ambiente globale è l'ambiente padre all'interno del quale viene valutata l'espressione globale.

L'espressione record-initializer-expression, ad esempio, valuta la sottoespressione per ogni campo con un ambiente modificato. Quest'ultimo include una variabile per ognuno dei campi del record, ad eccezione di quello inizializzato. L'inclusione degli altri campi del record consente ai campi di dipendere dai relativi valori, Ad esempio:

[  
    x = 1,          // environment: y, z 
    y = 2,          // environment: x, z 
    z = x + y       // environment: x, y
] 

Analogamente, l'espressione let-expression valuta la sottoespressione per ogni variabile con un ambiente che contiene ognuna delle variabili di "let", ad eccezione di quella da inizializzare. L'espressione let-expression valuta l'espressione che segue "in" con un ambiente contenente tutte le variabili:

let 

    x = 1,          // environment: y, z 
    y = 2,          // environment: x, z 
    z = x + y       // environment: x, y
in
    x + y + z       // environment: x, y, z

Da ciò risulta che sia record-initializer-expression sia let-expression definiscono in realtà due ambienti, uno dei quali include la variabile inizializzata. Questo concetto è utile per le definizioni ricorsive avanzate ed è trattato nella sezione Riferimenti a identificatori .

Per formare gli ambienti per le sottoespressioni, le nuove variabili vengono "unite" con le variabili nell'ambiente padre. Nell'esempio seguente vengono illustrati gli ambienti per i record annidati:

[
    a = 
    [ 

        x = 1,      // environment: b, y, z 
        y = 2,      // environment: b, x, z 
        z = x + y   // environment: b, x, y 
    ], 
    b = 3           // environment: a
]  

Nell'esempio seguente vengono illustrati gli ambienti per un record annidato con un'espressione let:

Let
    a =
    [
        x = 1,       // environment: b, y, z 
        y = 2,       // environment: b, x, z 
        z = x + y    // environment: b, x, y 
    ], 
    b = 3            // environment: a 
in 
    a[z] + b         // environment: a, b

L'unione di variabili con un ambiente può introdurre un conflitto tra le variabili, poiché ogni variabile in un ambiente deve avere un nome univoco. Il conflitto viene risolto nel modo seguente: se il nome di una nuova variabile da unire è identico a quello di una variabile esistente nell'ambiente padre, la nuova variabile avrà la precedenza nel nuovo ambiente. Nell'esempio seguente, la variabile interna (più profondamente annidata) x avrà la precedenza sulla variabile esterna x.

[
    a =
    [ 
        x = 1,       // environment: b, x (outer), y, z 
        y = 2,       // environment: b, x (inner), z 
        z = x + y    // environment: b, x (inner), y 
    ], 
    b = 3,           // environment: a, x (outer) 
    x = 4            // environment: a, b
]  

Riferimenti a identificatori

Per fare riferimento a una variabile all'interno di un ambiente, viene usato un identifier-reference.

identifier-expression:
      identifier-reference
identifier-reference:
      exclusive-identifier-reference
      inclusive-identifier-reference

La forma più semplice di riferimento a identificatore è un exclusive-identifier-reference:

exclusive-identifier-reference:
      identifier

Per un exclusive-identifier-reference non è corretto fare riferimento a una variabile che non fa parte dell'ambiente dell'espressione in cui è incluso l'identificatore.

Per un exclusive-identifier-reference non è corretto fare riferimento a un identificatore attualmente in fase di inizializzazione se l'identificatore a cui viene fatto riferimento è definito all'interno di una record-initializer-expression o let-expression. Per ottenere l'accesso all'ambiente che include l'identificatore in fase di inizializzazione, è invece possibile usare un inclusive-identifier-reference. Se usato in qualunque altra situazione, un riferimento inclusive-identifier-reference è equivalente a un exclusive-identifier-reference.

inclusive-identifier-reference:
      @ identifier

Questo è utile quando si definiscono funzioni ricorsive perché il nome della funzione normalmente non rientrerebbe nell'ambito.

[ 
    Factorial = (n) =>
        if n <= 1 then
            1
        else
            n * @Factorial(n - 1),  // @ is scoping operator

    x = Factorial(5) 
]

Come per un'espressione record-initializer-expression, un inclusive-identifier-reference può essere usato all'interno di un'espressione let-expression per accedere all'ambiente che include l'identificatore in fase di inizializzazione.

Ordine di valutazione

Si consideri l'espressione seguente che inizializza un record:

[ 
    C = A + B, 
    A = 1 + 1, 
    B = 2 + 2 
]

Quando viene valutata, questa espressione genera il valore di record seguente:

[ 
    C = 6, 
    A = 2, 
    B = 4 
]

L'espressione dichiara che, per eseguire il calcolo A + B per il campo C, i valori dei campi A e B devono essere noti. Questo è un esempio di ordinamento delle dipendenze dei calcoli fornito da un'espressione. L'analizzatore M rispetta l'ordinamento delle dipendenze fornito dalle espressioni, ma è libero di eseguire i calcoli rimanenti in qualsiasi ordine selezionato. L'ordine di calcolo potrebbe ad esempio essere:

A = 1 + 1 
B = 2 + 2 
C = A + B

In alternativa, potrebbe essere:

B = 2 + 2 
A = 1 + 1 
C = A + B

Oppure, poiché A e B non dipendono l'uno dall'altro, possono essere calcolati contemporaneamente:

    B = 2 + 2 contemporaneamente con A = 1 + 1
    C = A + B

Effetti collaterali

La possibilità che ha un analizzatore di espressioni di calcolare automaticamente l'ordine dei calcoli nei casi in cui non sono definite dipendenze esplicite nell'espressione è un modello di calcolo semplice e potente.

Questo modello si basa tuttavia sulla possibilità di riordinare i calcoli. Poiché le espressioni possono chiamare delle funzioni e queste ultime potrebbero osservare uno stato esterno all'espressione tramite l'esecuzione di query esterne, è possibile creare uno scenario in cui l'ordine di calcolo è rilevante, ma non è acquisito nell'ordine parziale dell'espressione. Si supponga, ad esempio, che una funzione legga il contenuto di un file. Se tale funzione viene chiamata ripetutamente, le modifiche esterne a tale file possono essere osservate e, pertanto, il riordino può causare differenze osservabili nel comportamento del programma. La dipendenza dall'ordine di valutazione osservato per la correttezza di un'espressione M determina una dipendenza da particolari scelte di implementazione che possono variare da un analizzatore al successivo oppure anche per lo stesso analizzatore in circostanze diverse.

Immutabilità

Una volta calcolato, un valore diventa immutabile, ovvero non può più essere modificato. Questo semplifica il modello per la valutazione di un'espressione e consente di ragionare più facilmente sul risultato perché non è possibile modificare un valore dopo che è stato usato per valutare una parte successiva dell'espressione. Un campo di record, ad esempio, viene calcolato solo quando necessario. Una volta calcolato, tuttavia, rimane invariato per tutta la durata del record. Anche se il tentativo di calcolare il campo ha generato un errore, lo stesso errore verrà generato nuovamente a ogni tentativo di accesso al campo del record.

Un'eccezione importante alla regola di immutabilità dopo il calcolo viene applicata ai valori di elenchi, tabelle e binari, che hanno una semantica di flusso. La semantica di flusso consente a M di trasformare contemporaneamente i set di dati che superano la memoria disponibile. Con il flusso, i valori restituiti durante l'enumerazione di un determinato valore di tabella, elenco o binario vengono generati su richiesta ogni volta che vengono richiesti. Poiché le espressioni che definiscono i valori enumerati vengono valutate ogni volta che vengono enumerate, l'output prodotto può essere diverso tra più enumerazioni. Questo non significa che più enumerazioni generano sempre valori diversi, ma che i valori possono essere diversi se l'origine dati o la logica M usata è non deterministica.

Si noti inoltre che l'applicazione di una funzione non equivale alla creazione di un valore. Le funzioni della libreria possono esporre lo stato esterno, ad esempio l'ora corrente o i risultati di una query su un database che si evolve nel tempo, e sono quindi non deterministiche. Anche se le funzioni definite in M non sono in grado, in quanto tali, di esporre un comportamento non deterministico, possono farlo se sono definite in modo da richiamare altre funzioni non deterministiche.

Una fonte rilevante di non determinismo in M è rappresentata dagli errori. Quando si verificano, gli errori interrompono le valutazioni (fino al livello in cui vengono gestite da un'espressione try). Non è in genere osservabile se a + b ha causato la valutazione di a prima di b o di b prima di a (ignorando qui la concorrenza per semplicità). Se, tuttavia, la sottoespressione che è stata valutata per prima genera un errore, è possibile determinare quale delle due espressioni è stata valutata per prima.