Concepts de base

Cette section décrit les concepts de base présentés dans les sections suivantes.

Valeurs

Un élément de données unique est appelé valeur. En gros, il existe deux catégories générales de valeurs : les valeurs primitives, qui sont atomiques, et les valeurs structurées, qui sont construites à partir de valeurs primitives et d’autres valeurs structurées. Par exemple, les valeurs

1 
true
3.14159 
"abc"

sont des primitives, car elles ne sont pas composées d’autres valeurs. En revanche, les valeurs

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

sont construites à l’aide de valeurs primitives et, dans le cas de l’enregistrement, d’autres valeurs structurées.

Expressions

Une expression est une formule utilisée pour construire des valeurs. Une expression peut être formée à l’aide d’une variété de constructions syntaxiques. Voici quelques exemples d’expressions. Chaque ligne est une expression distincte.

"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 forme d’expression la plus simple, comme illustré ci-dessus, est un littéral représentant une valeur.

Les expressions plus complexes sont générées à partir d’autres expressions, appelées sous-expressions. Par exemple :

1 + 2

L’expression ci-dessus est en fait composée de trois expressions. Les littéraux 1 et 2 sont des sous-expressions de l’expression parente 1 + 2.

L’exécution de l’algorithme défini par les constructions syntaxiques utilisées dans une expression est appelée évaluation de l’expression. Chaque genre d’expression contient des règles relatives à la façon dont il est évalué. Par exemple, une expression littérale comme 1 produit une valeur constante, tandis que l’expression a + b prend les valeurs résultantes produites en évaluant deux autres expressions (a et b) et les ajoute conformément à un ensemble de règles.

Environnements et variables

Les expressions sont évaluées dans un environnement donné. Un environnement est un ensemble de valeurs nommées, appelées variables. Chaque variable d’un environnement a un nom unique au sein de l’environnement, appelé identificateur.

Une expression de niveau supérieur (ou racine) est évaluée dans l’environnement global. L’environnement global est fourni par l’évaluateur d’expression, au lieu d’être déterminé à partir du contenu de l’expression en cours d’évaluation. Le contenu de l’environnement global comprend les définitions de bibliothèque standard, et peut être affecté par les exportations à partir de sections d’un ensemble de documents. (Par souci de simplicité, les exemples de cette section partent du principe que l’environnement global est vide. Autrement dit, on suppose qu’il n’existe aucune bibliothèque standard ni d’autres définitions basées sur une section.)

L’environnement utilisé pour évaluer une sous-expression est déterminé par l’expression parente. La plupart des genres d’expressions parentes évaluent une sous-expression dans le même environnement que celui dans lequel ils ont été évalués, mais certains utilisent un environnement différent. L’environnement global est l’environnement parent dans lequel l’expression globale est évaluée.

Par exemple, le record-initializer-expression évalue la sous-expression pour chaque champ avec un environnement modifié. L’environnement modifié comprend une variable pour chacun des champs de l’enregistrement, sauf celui qui est initialisé. L’inclusion des autres champs de l’enregistrement permet aux champs de dépendre des valeurs des champs. Par exemple :

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

De même, le let-expression évalue la sous-expression pour chaque variable avec un environnement contenant chacune des variables du let, sauf celle qui est initialisée. Le let-expression évalue l’expression qui suit le in avec un environnement contenant toutes les variables :

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

(Il s’avère que le record-initializer-expression et le let-expression définissent en fait deux environnements, dont l’un inclut la variable en cours d’initialisation. Cela est utile pour les définitions récursives avancées, et est couvert dans Références aux identificateurs.

Pour former les environnements pour les sous-expressions, les nouvelles variables sont « fusionnées » avec les variables dans l’environnement parent. L’exemple suivant montre les environnements pour les enregistrements imbriqués :

[
    a = 
    [ 

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

L’exemple suivant montre les environnements pour un enregistrement imbriqué dans un 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

La fusion de variables avec un environnement peut introduire un conflit entre les variables (puisque chaque variable d’un environnement doit avoir un nom unique). Le conflit est résolu comme suit : si le nom d’une nouvelle variable fusionnée est identique à celui d’une variable existante dans l’environnement parent, la nouvelle variable est prioritaire dans le nouvel environnement. Dans l’exemple suivant, la variable interne x (la plus profondément imbriquée) est prioritaire sur la variable externe 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
]  

Références aux identificateurs

Un identifier-reference est utilisé pour faire référence à une variable dans un environnement.

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

La forme la plus simple de référence d’identificateur est un exclusive-identifier-reference :

exclusive-identifier-reference :
      identifier

Un exclusive-identifier-reference ne doit pas faire référence à une variable qui ne fait pas partie de l’environnement de l’expression dans laquelle l’identificateur apparaît.

Il s’agit d’une erreur pour un exclusive-identifier-reference qui désigne un identificateur en cours d’initialisation si l’identificateur référencé est défini à l’intérieur d’une record-initializer-expression ou let-expression. À la place, vous pouvez utiliser un inclusive-identifier-reference pour accéder à l’environnement qui inclut l’identificateur en cours d’initialisation. Si une inclusive-identifier-reference est utilisée dans une autre situation, elle équivaut à une exclusive-identifier-reference.

inclusive-identifier-reference:
      @identifier

Cela est utile lors de la définition de fonctions récursives, puisque le nom de la fonction ne se trouve normalement pas dans l’étendue.

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

    x = Factorial(5) 
]

Comme c’est le cas avec un record-initializer-expression, vous pouvez utiliser un inclusive-identifier-reference dans un let-expression pour accéder à l’environnement qui inclut l’identificateur en cours d’initialisation.

Ordre d’évaluation

Examinez l’expression suivante qui initialise un enregistrement :

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

Quand elle est évaluée, cette expression produit la valeur d’enregistrement suivante :

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

L’expression indique que pour effectuer le calcul A + B pour le champ C, les valeurs des champs A et B doivent être connues. Il s’agit d’un exemple de classement des dépendances des calculs fourni par une expression. L’évaluateur M se conforme au classement des dépendances fourni par les expressions, mais il est libre d’effectuer les calculs restants dans l’ordre de son choix. Par exemple, l’ordre de calcul peut être :

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

Ou encore :

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

Ou, puisque A et B ne dépendent pas l’un de l’autre, ils peuvent être calculés simultanément :

    B = 2 + 2en même temps queA = 1 + 1
    C = A + B

Effets secondaires

Permettre à un évaluateur d’expression de calculer automatiquement l’ordre des calculs dans les cas où il n’existe aucune dépendance explicite indiquée par l’expression est un modèle de calcul simple et puissant.

Toutefois, cela s’appuie sur la possibilité de reclasser les calculs. Étant donné que les expressions peuvent appeler des fonctions et que ces fonctions peuvent observer l’état externe à l’expression en émettant des requêtes externes, il est possible de construire un scénario dans lequel l’ordre de calcul importe, mais n’est pas capturé dans l’ordre partiel de l’expression. Par exemple, une fonction peut lire le contenu d’un fichier. Si cette fonction est appelée à plusieurs reprises, les modifications externes apportées à ce fichier peuvent être observées et, par conséquent, le reclassement peut entraîner des différences observables dans le comportement du programme. Le fait de dépendre de ce classement d’évaluation observé pour l’exactitude d’une expression M génère une dépendance envers des choix d’implémentation particuliers susceptibles de varier d’un évaluateur à l’autre, voire avec le même évaluateur dans différentes conditions.

Caractère immuable

Une fois qu’une valeur a été calculée, elle est immuable, ce qui signifie qu’elle ne peut plus être modifiée. Cela simplifie le modèle d’évaluation d’une expression et facilite la justification du résultat puisqu’il n’est pas possible de modifier une valeur une fois qu’elle a été utilisée pour évaluer une partie suivante de l’expression. Par exemple, un champ d’enregistrement est calculé uniquement quand cela est nécessaire. Cependant, une fois calculé, il reste fixe pour la durée de vie de l’enregistrement. Même si la tentative de calcul du champ a généré une erreur, cette même erreur sera à nouveau levée lors de chaque tentative d’accès à ce champ d’enregistrement.

Une exception importante à la règle calculée une fois immuable s’applique à la liste, à la table et aux valeurs binaires, qui ont une sémantique de diffusion en continu. La sémantique de diffusion en continu permet à M de transformer des jeux de données qui ne tiennent pas dans la simultanément. Avec la diffusion en continu, les valeurs retournées lors de l’énumération d’une table, d’une liste ou d’une valeur binaire donnée sont produites à la demande chaque fois qu’elles sont demandées. Étant donné que les expressions définissant les valeurs énumérées sont évaluées chaque fois qu’elles sont énumérées, la sortie qu’elles produisent peut être différente entre plusieurs énumérations. Cela ne signifie pas que plusieurs énumérations entraînent toujours des valeurs différentes, mais qu’elles peuvent être différentes si la source de données ou la logique M utilisée est non déterministe.

Notez également que l’application de fonction n’est pas identique à la construction de valeur. Les fonctions de bibliothèque peuvent exposer un état externe (tel que l’heure actuelle ou les résultats d’une requête sur une base de données qui évolue au fil du temps), ce qui les rend non déterministes. Bien que les fonctions définies en M n’exposent pas, en tant que telles, ce genre de comportement non déterministe, elles le peuvent si elles sont définies de façon à appeler d’autres fonctions qui ne sont pas déterministes.

Les erreurs sont une source finale de non-déterminisme en M. Les erreurs arrêtent les évaluations quand elles se produisent (jusqu’au niveau où elles sont gérées par une expression try). En règle générale, il est impossible de voir si a + b a provoqué l’évaluation de a avant b ou de b avant a (en ignorant ici la concurrence par souci de simplicité). Toutefois, si la sous-expression qui a été évaluée en premier génère une erreur, il est possible de déterminer laquelle des deux expressions a été évaluée en premier.