Le bloc de code est un programme ou un sous-programme. C'est une instruction, ou une séquence d'instructions séparées par des points-virgules, et qui peut être délimité par des crochets `{}`. À chaque bloc de code correspond un contexte qui précise quelles sont les variables présentes.
Le programme principale est un bloc de code comme les autres. Il a beau être à la racine, être un ancêtre de tous les autres blocs de code. Il se peut qu'il s'incère comme sous-programme et deviennent un bloc de code placé dans un bloc parent, et qu'il puisse être appellé par un bloc d'appel.
Le langage doit être le plus bref possible et donc peut avoir plusieurs interprétations. Par exemple nous pouvons écrire `a"="b"+"3` sans autre déclaration. Un contexte implicite plus large précisera que `3` est un entier, que l'addition est un opérateur binaire interne commutatif, que l'égalité est un opérateur d'assignement de priorité syntaxique plus faible, que `a` et `b` sont des opérateurs nullaire. Et par inférence de type, l'interpréteur déduira que `a` et `b` sont des variables de type entier (ou d'un type plus large contenant les entiers et dans lequel l'opérateur binaire `"+"` y est défini).
Les variables `a` et `b` sont dites présentes dans le bloc de code. Et il y a plusieurs sortes de variables, plusieurs statuts.
Une variable locale peut être persistante ou non, selon que sont existence perdure ou non après l'exécution du bloc de code. Si elle perdure, elle est créée lors de la création du bloc de code, et se comporte comme un attribut du bloc de code. Puis elle sera retrouvée dans chaque exécution du bloc de code dans l'état où la dernière exécution du bloc de code l'aura laissée. Si elle n'est pas persistante alors elle est créée à chaque début d'exécution du bloc de code et détruite après chaque fin d'exécution du bloc de code.
Le bloc parent est celui exécutant l'instruction de création du bloc. Le bloc d'appel est celui exécutant l'instruction d'appel du bloc.
Une variable peut être locale, globale, ou d'appel, ou constituer un argument. Si la variable est locale elle est créées dans le bloc de code et n'est connectée à rien en amont, ou seulement à elle-même lorsqu'elle est persistante. Si la variable est globale, elle est connectée au bloc parent. Si la variable est d'appel, elle est connecté au bloc d'appel. Les arguments se différencient de ces dernières. Ce sont des variables créée localement, et connectées par adressage indirecte au bloc d'appel, par l'appel, et empilé dans la pile d'appel, ce qui permet à des blocs de code de s'appeller de façon récurcive.
Une variable, globale ou d'appel, peut servir d'entrée-sortie des données dans le sous-programme. Au cours du processus d'exécution, il se déploie une arborescence des créations de bloc, et une arborescence des appels de bloc.
Dans tous les cas, on évite de recopier les données d'entrée, car une données peut être de taille impondérable. On remplace autant que possible les transmissions de données, par des transmissions d'adresses. Néanmoins pour les arguments, on s'autorise des appels avec comme arguments des valeurs non contenues dans une variable. Dans ce cas, on crée implicitement et préalablement à l'appel, une variable générique pour recevoir la donnée, puis on transmet l'adresse de la variable générique, faisant que la transmission des données se fait toujours par adresse.
Ainsi les variables présentes dans le bloc ont 5 statuts possibles : non-persisant, persistant, globale, d'appel, argument. Et elles ont 4 rôles possibles, variable interne ou, variable d'entrée et/ou variable de sortie.
Puis dans un bloc, il est théoriquement possible que d'autres variables peuvent venir s'ajouter, provenant de blocs fils (arborescence des créations) ou de blocs appelés (arborescence des appels), et si ces variables ont le mêmes noms, ce sont les mêmes variables, elles établissent alors un lien de communication de données entre blocs fils ou bloc appelés.
On reprend la syntaxe de Ruby. On ajoute dans le bloc de code une déclaration des arguments à l'aide de l'opérateur de séquence, qu'est la virgule, et à l'aide de l'opérateur de séparation d'entête, `"|"`. Les arguments sont les variables choisies par l'appel dans le bloc d'appel, que l'on mémorise dans de nouvelle variable créée localement, et auxquelles ont accède par adressage indirecte.
`f "=" { x | x"∗"x"+"1} ; f(2)` `=` `5`
Par défaut, toutes les variables autre que les arguments sont globaux c'est à dire connectées au bloc parent. Et si on souhaite une variable locale, on la déclare avec `"var"`. Et si on souhaite qu'elle soit persistante, on la déclare avec `"att"` qui signifie "attribut". Et si on veut une variable connectée au bloc d'appel, on la souligne.
Qu'est ce qui guide ce choix ? Le nom d'une variable ou d'un élément est fait pour cela, pour qu'il servent de référence dans la suite du document. La variable est donc par défaut globale, connectée au bloc parent. Il faut donc pour crée un variable locale, fairer une déclaration. Les mathématiques illustre parfaitement cette gestion des domaines de nom de variable. Lorsque dans une formule logique apparait une sous formule dont la racine est `AAx` ou `EEx`, le quantificteur va créée une variable locale `x` qui, si elle existe déjà, va la masquer et va la remplacer localement sur toute la portée syntaxique du quantificateur qu'est la sous-formule.
Pour éviter les conflits de variables et la lourdeur des déclarations de localité des variable, un contexte plus large pourra spécifier que certaines variables, par défaut de déclaration, sont locales non-persistentes.
Pour circoinscrire les effets de bord, on peut formaliser un complément d'information sur chaque bloc de code précisant le rôle ; interne, entrée, sortie, ou entrée-sortie, de chaque variable présente.
Pour une variable d'entrée qui ne doit pas jouer le rôle de variable de sortie, on peut ajouter le type `"Const"` pour bloquer toutes éventuelles modifications de la variable en aval. Considérons les instructions suivantes :
`f "=" { x | x"∗"x"+"1} ; z"="2; f(z)` `=` `5`
La variable `f` contient un bloc de code qui est une fonction unaire. Si on tient à ce que l'argument `x` transmis à la fonction ne soit qu'une entrée seule, et qu'aucun code en aval ne puisse modifier l'argument `x`, alors on utilise le lock. La variable est dite vérouillée :
`f "=" { x | "lock" x; x"∗"x"+"1}`
Cela assure que quelque soit les éventuelles erreurs de conception du programme, la variable `x` ne peut pas être modifiée en aval.
Pour éviter la lourdeur des déclarations de vérouillage des variables, un contexte plus large pourra spécifier que les arguments et certaines variables, par défaut de déclaration, sont intitialisée à l'état vérouillé.
Pour créer une variable locale persistante, qui éventuellement masque une autre variable de même nom existante dans le bloc parent, on utilise l'opérateur `"att"`. Considérons le bloc de code suivant, que l'on mémorise dans la variable `f` :
`f "=" { x | "att" a,b; a"∗"x"+"b}`
Le bloc de code `f` est une fonction unaire qui transforme `x` en `a"∗"x"+"b`, et qui possède 2 variables locales persistantes `a` et `b`. Ces variables étant pérennes, Il faut alors mettre en place un dispositif pour pouvoir les initialiser, ou les lire, ou les modifier. Étant donné un bloc de code mémorisé par la variable `f`, on note `f"."a` l'accès à la variable locale persistante de nom `a`. Le bloc de code se comporte comme un objet ayant ses variables locales persistantes comme attributs.
Pour une variable locale persistante, l'initialisation et la modification non pas le même sens. L'initialisation fixe la valeur de la variable à la création du bloc de code, tandis que la modification modifie la variable à l'instant.
Une variable non initialisée sera interprétée comme un symbole. C'est pourquoi le bloc de code suivant, appliqué au nombre `3` va retourner une expression symbolique :
`{ x | "att" a,b; a"∗"x"+"b}(3) ` `=` `a "∗"3"+"b`
On peut initialiser ces variables pérennes par l'instructions `f"."a"="5; f"."b"="1`. Néanmoins une telle initialisation devrait pouvoir être faite dans le bloc de code lui-même. Cela se fait en utilisant le symbole des deux-points lors de la création de la variable :
`f={x | "att" a:5, b:1; a"∗"x"+"b}`
L'assignation de `a` par `5` et de `b` par `1` ne se fait qu'au moment de la création du bloc et de l'assignation du bloc de code à `f`. Notez que c'est l'assignation d'une copie du bloc de code qui constitue la création du bloc `f`.
Dans ces 4 derniers chapitres nous avons découvert les opérateurs suivants :
`"var"`Définition des variables locales. `"att"`Définition des attributs. `:`Initialisation des attributs. `"."`Accès aux attributs. `"lock"`Ajout du type `"Const"` aux variables.
On regroupe dans le type la définition des méthodes s'appliquant aux éléments de ce type. Par analogie avec un langage objet, les types jouent le rôle de classe, et les éléments jouent le rôle d'objet. Mais on préfère garder le nom de type et d'élément car on va prolonger notre analogie aux structures mathématiques.
Les éléments possèdent des méthodes qui sont regroupées dans leur type. Les types sont aussi des éléments et possède donc les méthodes commune à tous les éléments (méthodes obtenue par héritage), mais il sont spécialisés en étant membre du type de tous les type. Les types possèdent donc un certain nombre de méthodes spécifiques aux types, de la même façon que les classes possèdent des méthodes spécifique aux classes, et sont les objets d'une superclasse. Ces méthodes sont définies dans le type de tous les types, c'est à dire dans le type `"Type"` qui joue ainsi le rôle de superclasse.
La première méthode à définir concerne les types. C'est la méthode `"new"` pour créer un élément du type en question. Cette méthode s'appelle un constructeur. Ainsi quelque soit un type `A`, représenté sous forme d'un symbole, d'une variable ou d'une expression, l'instruction `A".new"()` crée un élément de type `A`, avec des paramètres initialisateurs par défaut.
Il faut toujours présenter l'opération qui offre le plus grand choix, qui soit la plus démonstrative, et de le faire par défaut afin que le développeur n'est pas besoin de saisir un long code pour pouvoir explorer et exposer toutes les possibilités. C'est pourquoi la méthode new doit pouvoir s'utiliser sans argument. À cet égard le nom même de cette méthode `"new"` est peut-être superflu. On peut identifier cette méthode à un appel fonctionnelle, faisant que :
`A"(...)" = A".new(...)"`
où les `"..."` désigne une séquence d'éléments. Cela est possible car avant cette définition, un type n'était pas une fonction. Et donc on peut donner au type, un second rôle de fonction, pour lancer sa méthode `"new"`.
La seconde méthode à définir concerne les éléments. C'est la méthode d'affichage `"aff"` qui convertit l'élément en un bloc de code dont l'éxecution produit une copie de l'élément où l'élément lui-même si c'est un élément qui ne se duplique pas. Et cette méthode doit tenir compte du contexte car l'exécution d'un bloc de code dépend du contexte.
On s'inspire des déclarations de fonction dans Haskel. Une fonction peut se définir par morceau à l'aide d'un selecteur d'arguments par unification. Par exemple considérons la fonction factoriel `f` définie mathématiquement comme suit :
`f = { 1 "↦" 1, x"↦"xf(x"-"1) "/" EEx }`
Cette fonction se définie informatiquement comme suit :
`f = {1|1}{x|x"∗"f(x"-"1)}`
Lors de l'exécution de l'instruction `f(3)`, l'interpréteur tente d'unifier `2` à `1`, puis tente d'unifier `2` à `x`. Il y arrive en associant à `x` la valeur `2`, et retourne l'évaluation de `2"∗"f(1)`. Lors de l'execution de l'instruction `f(1)`, l'interpréteur unifie `1` à `1` et retourne `1`. L'ordre des blocs est important, car c'est le premier bloc où l'unification réussie qui sera exécuté.
L'entête peut alors être davantage sophistiquée. Au lieu de contenir une séquence de nom de variables distinctes, elle peut contenir une liste d'expressions symboliques non nécessairement distinctes. L'unification se fait après évaluation des arguments, faisant que toute expression composée d'opérateurs connus non symboliques est évalué avant l'unification. Par exemple l'entête `x"+"y` ne peut pas s'unififer à l'argument `2"+"3` car celui-ci sera d'abord évalué en `5`. Parcontre l'argument `2"+"z` dans le cas où l'opétrateur `"+"` est symbolique ou que l'opérateur `z` est symbolique, sera évalué en la même expression symbolique, et l'unification fonctionnera en associant à `y` la valeur symbolique `z`.
Le bloc de code possède une arité d'entrée qui est son nombre d'entrées présentes dans l'entête. Considérons un bloc de code à `3` entrées :
`x,y,z|x"∗"y"+"z`
Ce bloc de code est identique à la fonction suivante :
`(x,y,z) ↦ x"∗"y"+"z`
Son arité est `3`. Le bloc de code se comporte alors comme une fonction ternaire. On mémorise le bloc de code dans une variable `f` :
`f={x,y,z|x"∗"y"+"z}`
Délors `f` est une fonction ternaire. L'appel se fait en collant une paire de parenthèse à `f` et en énumérant entre les parenthèses `f"(...)"` les arguments séparés par des virgules. On note par exemple `f(2,3,4)` l'application du bloc de code `f` aux arguments `2,3,4` et cela retourne `10`.
L'expression `2,3,4` est le contenue de la liste `(2,3,4)` et ne peut être placé que là où on attend une séquence d'arguments, c'est à dire dans un appel `n`-aire. Et on verra plus-tard qu'il peut y avoir des appels bis-`n`-aire. Dans le cas d'une fonction `n`-aire, le type d'entrée de la fonction est un type de liste dans lequel on retire un niveau de parenthèse extérieur, et qui constitue un type de séquence. On crée ainsi un second type de liste appelé type de séquence. L'opérateur `"@"` appliqué à une liste, la transforme en séquence. Puis on constatera qu'il est utile, pour rendre plus lisible les programmes, de distinguer par un tilda les variables contenant des séquences de celles n'en contenant pas. Notez que ce tilda n'est pas un opérateur, simplement un complément de nom qui indique que la variable et de type séquence. Ainsi `x` et `tilde x` sont deux variables distinctes qui n'ont aucune relation entre-elle :
`w = (2,3,4)`
`tilde x = "@"(2,3,4) `
`tilde x = "@"w`
`w = (tilde x)`
On parlera de la séquence `tilde x` et de la liste `w`. Noter que le créateur de liste `(...)` est une fonction sans nom, `n`-aire, et utilisant les parenthèses communes. Il ne doit pas y avoir d'opérateur collé à gauche car par exemple l'expression `f(...)` désigne l'appel d'une fonction sur la séquence d'arguments passés entre les parenthèses.
`f={x,y,z|x"∗"y"+"z}`
`f(tilde x) = f("@"w)= f("@"(2,3,4))= f(2,3,4)`
`g={(x,y,z)|x"∗"y"+"z}`
`g(w) = g("("2,3,4")") = g( ( tilde x ) ) = g( ( "@"w) ) `
Si le bloc de code n'a pas d'entête, son arité est nulle. Le bloc de code se comporte alors comme un opérateur nullaire. Néanmoins il peut être éxecuté. Par exemple, `f"="{5"+"10}`. L'éxecution de `f` se note par l'instruction `f()` est donne comme résultat `10`, alors que la simple instruction `f` va retourner le bloc de code `{5"+"10}`.
En mathématique les listes de tailles quelconque d'éléments appartenant à `A` se note à l'aide de l'opérateur de Kleene, `A^"∗"`, et il existe aussi une autre notation qui exclut la liste vide, `A^"+"` et qui représente donc l'ensemble de toutes les listes non vides d'éléments de `A`. On ajoute à cela une troisième notation qui exclut les listes vides et les listes singletons, `A"^"`, et qui représente donc l'ensemble de toutes les alternatives d'éléments de `A` d'au moins deux éléments.
Le cinquième type fondamentale est la liste d'éléments d'un certain type. Mais on ne va pas utiliser un nom spécifique pour le désigner. On utilise directement un opérateur constructeur unaire `"List"`, qui appliqué à un type `T` produit le type séquence d'éléments de type `T`, noté `"List"(T)`. Et si l'on répéte l'opération on obtient une arborescence à deux niveaux :
`"List"("List"(T))`
La méthode new du type liste est identifié à l'appel fonctionnelle, comme dans de nombreux types, et pour le type liste d'éléments la création peut s'écrire simplement par :
`"List"(Omega)".new"(...) ` `=` ` "List"(Omega)(...) ` `=` `(...)`
`"List"(Omega)(2,3,4) ` `=` `(2,3,4)`
Et la création de liste peut s'écrire plus simplement encore, comme une fonction sans nom, `n`-aire, utilisant les parenthèses communes, `(...)`. Mais attention, il ne faut pas qu'il y est un opérateur collé à gauche. Car par exemple l'expression `f(...)` désigne l'appel d'une fonction sur la séquence d'arguments passés entre les parenthèses.
Néanmoins dans cette écriture dénudée, l'inférence de type va jouer tout son rôle et donner un type de liste plus précis selon le type des éléments présents.
--- 14 mai 2021 ---