Suivant

Langage de programmation idéal
(2ième approche)


Langage de programmation idéal (1)

 

1) Introduction

Dans une approche naïve, on peut penser qu'il existe une démarche constructive qui s'impose, amenant à la conception d'un langage informatique universel, la pierre philosophale des informaticiens. La voix en est l'ensemencement par les mathématiques qui ont déjà un langage universel et qui sont déjà ensemencées par l'informatique au point où de nombreux concepts informatiques se retrouvent en mathématiques et vice versa. Il suffit de prolonger cette fusion. Cela consiste en un langage de calcul formel généraliste. Le paradigme existe, il est là, à porté de main, nous en voyons les effets dans les différents langages de calcul formel qui se sont développés dans le monde du libre, et dans les démonstrations mathématiques qui ont recours de plus en plus à l'informatique. C'est en cherchant à chaque fois le caractère unique, la rainure qui réunit toutes ces démarches constructives en une seule construction que l'on découvrira cette pierre philosophale supposée. Et ce sera aussi celle des mathématiciens car ce langage informatique universel constituera un démonstrateur de théorème aux capacités démultipliées. Le mariage de l'informatique et de la logique produira un langage informatique universel. C'est le pari que nous faisons. Mais pour l'instant, nous sommes loin de produire un résultat unificateur. Il nous faut d'abord convaincre de nombreux homologues de vouloir développer ces langages de calcul formel en explorant ce paradigme, et cela se fait en proposant des éléments concrets. Pour ceux qui sont davantage théoricien que praticien, on pourra commencer à lire le livre de Gilles Dowek & Jean-Jacques Lévy "Introduction à la théorie des langages de programmation" qui explore les différents paradigmes programmatifs. Mais il est possible même pour des personnes non-initiés de tout-de-suite se plonger dans une telle construction, et c'est ce que nous allons faire pour en donner le goût.

La conception d'un langage de programmation idéal va de paire avec celle du compilateur qui peut le traduire en langage machine. Mais le langage machine n'est pas-du-tout pratique à utiliser. En appliquant l'adage linuxien suivant : "Tout problème complexe doit pouvoir se subdiviser en plusieurs problèmes plus simples", on voit qu'il est possible de passer par un langage intermédiaire, et donc de procéder à une première compilation transformant le langage idéal en ce langage intermédiaire, puis de compiler en suite ce dernier à l'aide d'un compilateur déjà existant et qui a fait ses preuves.

On choisie comme langage intermédiaire le langage Golang car il possède les mêmes qualités essentielles que le langage C avec lequel Linux a été programmé, tout en étant beaucoup plus pratique. Et si on s'intéresse non pas à des programmes mais à des shells et à l'aspect interpréteur, alors on choisie comme langage intermédiaire le langage de script Ruby utilisant le shell irb (Interactive Ruby Shell) qui est orienté objets et qui est beaucoup plus patique que le shell Bash propre à Linux. Nous souhaitons développer un compilateur universelle d'une façon progressive à partir de quelques éléments de programmation de base que nous offre un système d'exploitation Linux, et qui sont :

1- Le compilateur Golang

2- Le shell Ruby

Et on proposera des versions du plus simple compilateur jusqu'au plus évolué des compilateurs, tel le développement d'un organisme passant d'une mue à l'autre, se transformant par étape, pour acquérire des fonctionnalités nouvelles et mettre en oeuvre des mécanismes linguistiques nouveaux.

1- L'étude des langages formels nous propose comme première étape, le langage terminologique monotype, aussi appelé une terminologie monotype, composé d'opérateurs d'arité fixée se combinant en des compositions closes appelés termes. Et au départ on ne conçoit qu'un seul type d'argument appelé valeur. C'est le premier choix, une première étape incontournable dans l'appréhension des langages formels, la maitrise du langage terminologique monotype. Voir Logique. Un tel langage engendre une structure mathématique libre composé d'un unique ensemble sous-jacent puisque le langage est monotype.

2- Puis le langage se perfectionne pour utiliser des variables qui sont toutes de même type. Le langage devient déclaratif, introduisant la déclaration de variable, étendant ainsi le langage dans des sous-contextes qui s'emboitent, formant des arbres de contextes déclaratifs. La variable contient une valeur ou un terme qui doit être évalué en une valeur. La variable comme le terme peut produire un terme et nécessiter de réiterer l'évaluation. Le rapport à la logique est directe : Le langage engendre la structure libre, la variable constitue une extension libre de la structure.

3- Un terme représente un calcul produisant une valeur. Comme nous souhaitons optimiser la complexité des calculs, on ne peut pas rester sur un modèle de calcul ne proposant qu'un seul résultat à la foi. Le troisième choix sera une extension du langage terminologique permettant des séquences de valeurs comme résultat, et donc en donnant également un moyen pour ré-aiguiller arbitrairement ces valeurs dans les différentes entrées des opérateurs.

4- Les opérateurs peuvent être variable constituant des variables d'arité non-nulle, permettant ainsi de définir des applications. Puis ces opérateurs peuvent proposer d'abord une unification de la liste de leurs arguments avec un modèle, définissant ainsi des fonctions, et peuvent se définir par morceau. Comme le langage se veut monotype il ne peut y avoir de distinction syntaxique entre termes du langage, et donc tout terme est suceptible de s'appliquer à une séquence d'arguments pour produire une séquence d'arguments.

5- Le langage change de nature et devient le langage alpha monotype. Le langage se définit par une grammaire ou une axiomatique. Plusieurs choix de grammaire ou d'axiomatique sont possibles selon les propriétés que l'on exige. Un bloc de code définit un opérateur, et on verra qu'il définit également un objet avec son langage.

6- Les opérateurs peuvent être compilés constituant alors des valeurs. Apparaît alors un langage de bas niveau que sont toutes ces valeurs obtenues par compilations et dont l'exécution se comporte comme un moteur de bas niveau de protocole, faisant que notre langage universel se construit sur un sous-langage de niveau de protocole plus bas.

7- Puis le concept de variable engendre celui de pointeur. La variable contient alors une valeur, un terme ou une addresse qui désigne soit une valeur, un terme ou encore une addresse, construisant ainsi une branche d'un arbre convergeant sur une racine qui sont toutes des valeurs ou des termes puisque nous sommes dans une terminologie monotype. Le septième choix est d'implémenter ces pointeurs qui permettront la conception de nombreux algorithmes de complexité linéaire.

8- Puis on ajoute les spécificitées propres à un shell que sont la gestion des processus, des mémoires partagées, et des canaux entre processus. Voir La machine vue par le haut. C'est le huitième choix, introduisant le concept de processus s'exécutant de façon concurrente aux autres processus et qui descend d'un processus père, et introduisant le concept de mémoire partagée entre plusieurs processus, et introduisant le concept de canal entre processus assurant le transfère de données d'un processus à un autre.

La variable s'identifie à un canal qui ne transmet et qui ne reçois qu'un seul terme ou qu'un seul pointeur ou qu'une seule valeur. La séquence d'argument peut également s'identifier à un canal qui en entrée reçoit les arguments (termes ou pointeurs ou valeur), et en sortie transmet les résultats (termes ou pointeurs ou valeur). Nous appellerons ce nouveau type de langage simplement un langage shell monotype alpha ou polonais.

9- Le langage que nous voulons définir est polymorphe c'est-à-dire qu'il change de forme en cours de route selon le contexte, pouvant ainsi toujours s'adapter au problème. C'est le neuvième choix majeur que nous ferons et qui consiste à déclarer des opérateurs constants qui peuvent masquer les précédant et à introduire une définition modifiable de leur syntaxe dans le contexte en cours. Et cela doit pouvoir s'établir par de simples instructions. D'où le caractère universelle prétendu de notre compilateur.

A chaque étape, le compilateur proposé doit constituer un outils pertinant, bien fait, bien programmé, utilisable telle quelle. Ainsi la construction n'est pas ingrate et motive ses auteurs. C'est là un point essentiel pour la faisabilité et la pérénité du projet de développement, voir développement holistique.

A chaque étape, se présentera un compilateur et son langage, de nom L, complété par un numéro d'étape noté en un seul chiffre. À certaine étapes majeur nous rencontrerons différents choix possibles exclusifs, formant une fourche dans l'évolution des versions et qui sera noté par une lettre majuscule A ou B, ou C... suivi par un numéro d'étape toujours noté en un seul chiffre, etc..., puis le tout pouvant être suivi par un trait d'union et un numéro de version de développement instable.

Pour faire les bons choix il convient évidement de préparer le terrain, d'explorer loin devant différentes notions et concepts afférents aux langages formels, d'entretenir une reflexion éclectique, ouverte, ce que nous faisons dans les discussions ci-dessous :

  1. Un langage de programmation polymorphe et contextuel
  2. Langage de programmation idéal   (2)   (3)
  3. Le langage informatique hardware idéal
  4. Genèse   (2)
  5. Langage formel
  6. La logique formelle
  7. Structures finies, langage et théorie    Structures d'égalité   premier ordre    espace
  8. Langage Alpha
  9. langage d'opérateurs
  10. Langage et calcul
  11. Implémentation des langages d'opérateurs
  12. Langage formel et hiérarchie de Chomsky
  13. Les fondements de l'informatique, et l'énumérabilité
  14. Logique

2) Terminologie monotype

La terminologie se définit en énumérant les opérateurs générateurs qui doivent être d'arité fixe. Par exemple considérons le langage `E` de présentation suivante :

`a,b,f"(.)",g"(.,.)"`

La présentation est une séquence d'opérateurs générateurs du langage. Le suffixe `"(.)"` rappelle que l'opérateur est unaire, et le suffixe `"(.,.)"` que l'opérateur est binaire. Le langage `E` est l'ensemble des termes qu'il est possible de composer avec ces opérateurs répliqués autant de fois que l'on veut :

`E = {a,b,f(a),f(b),g(a,a),g(a,b),g(f(a),a),...,g(f(b),g(a,a)),...}`

On définie un meta-langage permettant de définir le langage. La liste des opérateurs entourés de crochet `"< >"` engendre l'ensemble des termes qu'il est possible de composer avec ces opérateurs répliqués autant de fois que l'on veut :

`E="<"a,b,f"(.)",g"(.,.)>"`

Vous remarquerez que dans ce métalangage les opérateurs énumérés peuvent soit être de nouveaux opérateurs qui initient ce que l'on appelle en mathématique une extension élémentaire qui est ici libre, ou soit être déjà définie dans le contexte. Une autre façon de définir le langage ce fait à l'aide d'une grammaire que l'on note sous forme d'une définition récurcive d'ensembles :

`E={a,b,f(E),g(E,E)}`

L'ensemble `E` désigne l'ensemble sous-jacent de la structure, et donne un moyen de construction de tous les termes du langage. Si `a,b,f("."),g(".,.")` sont de nouveaux éléments et n'ont donc pas de définition, alors `E` est une structure libre.

Afin que le langage exprime autre chose que lui-même, et d'en maitriser toute la liberté, on pose la distinction entre deux mondes, le monde du langage qui est l'ensemble des termes que l'on peut écrire, et le monde des valeurs qui peuvent être désignées par les termes. Le monde des valeurs désignées représente la structure. Le terme est le signifiant, la valeur qui constitue un élément est le signifié. Mais le terme désigne davantage qu'une valeur ou un élément, il désigne un procédé de calcul par composition d'opérateurs qui va produire cette valeur.

`a,b` sont des termes nucléïques et désignent des valeurs, et chaque terme désigne une valeur, et toutes les valeurs sont d'un seul type, c'est pourquoi le langage et dit monotype, et que la structure est basée sur un unique ensemble sous-jacent regroupant toutes les valeurs atteignables. Mais plus précisement, le terme désigne un calcul qui aboutit à cette valeur. Le terme nucléïque `a` fait partie du monde du langage, et sa valeur fait partie du monde réel et constitue un élément de la structure. L'interprétation d'un terme tel que `g(a,g(b,a))` consiste à faire le calcul que représente le terme. Et il se peut que deux termes d'expression différentes dans notre langage désignent la même valeur, et donc soient égaux en valeur. Ainsi `a` et `b` qui constituent des termes nucléïques d'expression différentes peuvent être égaux s'ils désignent la même valeur.

Parcontre, si les opérateurs générateurs n'ont pas de définition, alors ils désignent eux-même, et la structure engendrée est libre, c'est-à-dire que deux termes distincts désignent forcement deux éléments distincts. Dans ce cas, la valeur du terme est le terme lui-même mais perçu comme une chaine de caractères constituant un élément du monde réel et non comme un terme le désignant.

Paralèllement à la structure mathématique du langage, celui-ci s'enrichie de plusieurs syntaxes. Dans les cas simples, l'opérateur est reconnue par un nom court qui est une chaine de caractères, et il possède une arité. Prenons par exemple l'opérateur `g` d'arité 2. Il peut prendre plusieurs formes d'appel comme par exemple `g(x,y)` ou bien `xgy` à condition que des règles syntaxiques permettent de séparer les noms de variable, ou alors en utilisant des blancs comme séparateurs `x g y`. Il peut également prendre la forme `g x y` ou bien encore `x y g`, et aussi d'autres formes utilisant plusieurs chaines de caractères différentes tel que par exemple `"<"x:y">"` utilisant trois chaines de caractère `"<"`, ` ":"`, ` ">"` comme nom. La syntaxe n'est pas superfétatoire, elle permet d'adapter le langage au problème et constitue en cela une étape importante de sa résolution.

L'opérateur possèdera aussi un nom long constitué d'une unique chaine de caractères, et qui pourra servir aussi pour désigner l'opérateur et pour certaines formes d'appel. Nous aborderons la syntaxe en regardant les premières constructions qu'il est possibles de faire. La syntaxe par défaut sera la forme d'appel fonctionnel anglaise utilisant le nom court unique ou le nom long telle que par exemple `g(x,y)`. Chaque opérateur possède une syntaxe qui est ainsi codifiée, et l'ensemble de ces opérateurs avec leurs différents syntaxes choisies constituent la définition du langage.

3) Monde du langage et monde réel

Dans un ordinateur contemporain, toute donnée binaire brute est une succession contiguë d'octets référencés par l'adresse de son premier octet. Si la donnée est mémorisée dans une mémoire linéïque sans protocole de bas niveau pour indiquer sa taille ou sa fin, alors sa taille doit être préalablement connue et transmise ou alors on doit pouvoir déterminer sa fin en lisant la donnée par son début. Dans le cas général on doit mettre en oeuvre la première option, il faut transmettre la taille de la donné binaire puis la donnée binaire proprement dite.

On remet en cause la pertinence de la distinction entre fichier texte et fichier binaire pour proposer des fichiers texte qui mélangent textes et binaires. Cette remise en cause s'appuit sur l'importance d'une construction dense des données. Un texte n'est pas dense, mais notre langage étant polymorphe le texte pourra quand même être réduit en taille, et constituer le compromis pertinent entre densité, lisibilité par un humain, et programmacité, cette dernière notion désignant la capacité à intégrer dans la donnée elle-même des éléments de programmation de différents niveaux de protocoles.

Le fichier texte devient un fichier mixte texte-binaire. Il doit néanmoins pouvoir déterminer par une simple lecture qu'elles sont ses parties binaires et de quel type pour pouvoir les imprimer selon leur type. Chaque partie binaire est annoncée dans la partie texte qui le précède par son type et éventuellement sa taille si le type ne permet pas de déterminer sa fin en lisant la donnée par son début. D'où la naissance d'une typologie associée au hardware. On prédéfinit 3 types liés au harware :

  1. Le type chaine de caractères : On utilise le caractère guillemet " suivi de la chaine de caractères puis suivi du caractère guillemet ", la chaine ne devant pas contenir de caractère guillemet ". La chaine proprement dite comprend tous les caractères compris entre les guillemets et ne comprend les guillemets. Elle est mémorisée selon la norme Unicode et utf-8. (Type String).
     
  2. Le type binaire générale avec une méthode d'affichage hexadécimale : On utilise le caractère précédé d'un nombre `n` et suivi de la données binaires de taille `n` octets. La donnée binaire est affiché sous forme hexadécimale. (Type Brut(`n`)).
  3. Le type entier naturel de taille variable. Une succession d'octets dont le derniers bit est à 1 sauf pour le dernier octets. Cela permet de définir un nombre entier en gaspillant un bit lors de chaque utilisation de 7 bits de données. L'entier est affiché sous forme décimale. (Type Entier).

Nous avons désigné deux mondes, le monde du langage qui contient les termes, et le monde réel qui contient les valeurs. Un terme calcul une valeur. Mais un terme se mémorise en une donnée binaire brute correspondant à sa chaine de caractères, qui est répertoriée par l'adresse de son premier octet, et qui constitue une valeur. Si la suite d'octets est un terme, c'est une chaîne de caractère qui obéïra aux normes unicode et utf-8 et à la syntaxe du langage. Il n'y aura pas besoin d'indiquer la fin du terme puisque celle-ci est indiquée par la syntaxe du terme lui-même. Parcontre, si la suite d'octets est une valeur quelconque c'est à dire une données binaire quelconque, rien n'indique sa fin. C'est cette difficulté qui nous fait vouloir typer les données bruts.

  1. L'opérateur guillemet " " inscrit directement la données texte dans le fichier texte. Le texte est alors perçu comme du texte et non comme un terme. Par exemple l'inscription de la chaine "g(a,b)" ce fait par l'expression suivante : "g(a,b)". L'opérateur retourne l'adresse du début de la chaine, c'est l'adresse de l'octet codant le caractère g, en tant qu'objet de type String sachant que le caractère définissant le début et la fin de la chaine est le guillemet " et qu'il ne fait pas partie de la chaine de caractères.
  2. L'opérateur inscrit directement la succcession de chiffres hexadécimales sous forme de données binaires brutes. Par exemple l'inscription de deux octets contenant 15 ce fait par l'expression suivante, et nécessite la chaine 2† en préfixes qui utilise 3 octets. "`2"†"000F`. L'opérateur retourne l'adresse du début de la donnée brute en tant qu'objet de type Brut de taille 2. Notez que la taille est mémorisé dans le type et non dans la donnée.
  3. L'opérateur inscrit directement la succesion de chiffres décimaux en entier binaire de taille variable (le septième bits de l'octet servant à déterminer la fin de la chaine. Par exemple : `"‡"256` correspond à deux octets écrits en binaire 0000 0001 1000 0000 et notés en héxadécimal par 0180. L'opérateur retourne l'adresse du début de la donnée brute en tant qu'objet de type Entier.

Autrement dit nous avons l'égalité de contenu entree ‡256 et 2†0180 sans avoir l'égalité de type, le premier est de type Entier, le second et de type Brut(2). Mais dans notre langage monotype il n'y a pas de distinction de type fait par l'utilisateur, les deux valeurs sont donc égale :

`"‡"256 == 2"†"0180`

Cependant une gestion de pseudo-types peut être faite par les opérateurs. En particulier f("a") n'a pas la même signification que f(a). L'évaluation de f(a) commence par calculer le terme a et à le remplacer par sa valeur puis à exécuter f avec cette valeur comme argument, tandis que l'évaluation de f("a") va exécuter f avec comme argument la chaine de caractère "a" qui constitue une valeur.

Le résultat d'un programme étant le plus souvent une valeur, celle-ci pourra toujours s'exprimer directement par une expression binaire brute à l'aide de l'opérateur . Ainsi par exemple si l'évaluation du terme `f(a)` produit la valeur d'un mot binaire contenant 15, alors le shell affichera le terme générique `2†000F` comme résultat. Et dans chaque expression où `f(a)` apparait, il pourra être remplacé par sa valeur. Ainsi `g(f(a),b)` pourra s'écrire `g(2†000F,b)`. Néamnoins la valeur exprimée ainsi est à la fois plus encombrante et plus difficile à lire, c'est pourquoi on donnera des noms aux valeurs que nous jugerons utiles. Ces noms sont des opérateurs nucléïques ajoutés au langage et constituront ainsi des constantes initialisée à ces valeurs. Ces noms seront utilisé automatiquement prioritairement pour remplacer l'expression hexadécimale. Et s'il y a plusieurs noms désignant la même valeur, alors c'est le contexte du langage qui choisira le nom à utiliser.

Les données textes de type String sont des successions de caractères unicodes, une norme internationale qui couvre les écritures du monde entier, et on a choisit la norme utf-8 qui occupe le moins de place en nombre d'octets. Ainsi, une donnée brute de texte est une succession d'octets respectant les normes unicode et utf-8, une norme universellement partagée. On définit un opérateur spécial qui va délimiter ces valeur de données textes réels. Ce sera simplement un constructeur de chaine de caractères avec une syntaxe habituelle de définition en entourant la chaine de caractères par des guillemets. La seul contrainte de cette méthode, c'est que le caractère guillemet ne doit pas être utilisé dans la chaine de caractères.

Afin de pouvoir quand-même utiliser le caractère guillemet dans la chaine de caractère, on utilise un caractère d'achoppement qu'est l'antislash \. Celui-ci a pour effet d'annuler la fonction spéciale du caractère suivant, Ainsi pour mémorisé une chaine de caractères telle que ab"c"de nous écrirons `"ab\"c\"de"` et pour afficher l'antislash, par exemple pour mémoriser une chaine de caractères telle que ab\cd nous écrirons "ab\\cd".

On peut avoir l'impression de bégayer. Pourquoi vouloir générer de l'unicode utf-8 alors que notre langage utilise déjà l'unicode comme alphabet et est implémenté en utf-8 ? C'est la distinction entre le terme appartenant au langage qui ne se préocupe pas de savoir sous quel forme binaire il est implémenté dans la machine, et la forme sous la quelle il est mémorisé. Ainsi le terme "g(a,b)" constitue la forme binaire sous laquelle le terme g(a,b) est mémorisé.

Comme nous faisons le choix d'un langage monotype et que nous procédons à des extensions toujours monotype, la distinction ne peut s'opèrer que par des états différents de la procédure de traitement. L'absence de type ne veut pas dire qu'il n'y a pas de type mais que c'est la procédure de traitement qui gère de façon transparante les types de telle sorte que pour l'utilisateur il n'y a pas de type.

4) Terme et valeur

La construction du langage terminologique monotype va nous permettre d'en percevoir les principes de base et leurs développement possibles, liant le langage (l'ensemble des termess) et le monde réel (l'ensemble des valeurs ou éléments). Dans notre langage, le terme correspond à une instruction et donc à un programme. S'il est compilé, il devient une valeur et donc un élément.

L'opérateur normal retourne une valeur sauf s'il n'arrive pas à évaluer l'un de ses arguments ou lui-même, auquel cas il retourne le terme qu'il engendre en remplaçant tous les arguments évaluables par leur valeur. Et certains opérateurs spéciaux retourne non pas une valeur mais un terme. Donc un terme retourne une valeur ou un terme. On accorde donc cette faculté également aux variables. On agrandit ainsi le langage en donnant la possibilité aux variables et aux opérateurs de retourner non-pas une valeur mais un terme. Cela signifit que le résultat peut être un terme et donc un programme, et donc qu'il peut subire une deuxième évaluation si on attend de lui une valeur. Et dans ce cas, comme nous somme dans un langage monotype, cette seconde évaluation s'effectue automatiquement afin de retrouver une valeur du même type. Et si cette évaluation redonne un terme alors il faut réitérer l'évaluation du résultat jusqu'à obtenir une valeur, ou bien considérer que le calcul ne peut pas aboutir et retourner dans ce cas le terme engendré en remplaçant les arguments évaluables par leur valeur. Et de façon plus complète, il engendre un ensemble de tels termes qui lui sont égaux.

Pour l'utilisateur, il ne peut y avoir de distinction entre un terme produisant un terme et un terme produisant une valeur, ou plus exactement, c'est l'opérateur qui mettra en oeuvre des traitements différents selon que l'argument est un terme ou une valeur, et selon les attendus.

5) Introduction des variables

On introduit les variables comme suit. Tout nouveau nom d'opérateur qui ne fait pas partie des noms d'opérateur générateur désignera une variable. C'est un moyen qui évite des déclarations verbeuses, mais ces déclariations existent belles et biens, ce qui étend le langage terminologique en un langage déclaratif.

On commence par décrire la variable globale c'est à dire dont la déclaration est global au programme. Chaque déclaration de variable va ajouter au langage un opérateur nullaire, mais variable, c'est à dire que cet opérateur nullaire possède un comportement supplémentaire, celui de pouvoir changer la valeur qu'il désigne. Autrement-dit, les opérateurs nullaires du langage initial sont qualifiés de constants, tandis que les opérateurs nullaires déclarés après coup comme variable sont qualifiés de variables.

La variable contient une valeur ou un terme ou seulement une référence statique, qui peut être soit mémorisé ou soit le résultat d'un calcul. Dans ce dernier cas la variable mémorise un programme qui calcul sa valeur ou le terme en question, ou la référence statique en question.

Lorsque la variable est effacée, on peut considérer soit qu'elle contient une référence dynamique à elle-même, ou bien qu'elle contient un terme qui est elle-même. Ainsi la variable `x` une fois déclarée, contient le terme `x`. Ce choix programmatif mineur n'a pas de pertinence particulière sauf dans le cas où l'on veut donner plus-tard un sens à des contenues récurcifs tel une variable `x` contenant le terme `f(x)`. Lorsque la variable `x` est libre, elle constitue un nouvel élément telle une extension élémentaire.

L'affectation d'une valeur à une variable se fait par une instruction d'égalité. On utilise l'opérateur spécial d'affectation, de nom long affectation, et qui s'applique à deux arguments. Le premier argument est une variable, et le second argument est un terme. Puis on le dote d'une syntaxe centrée de priorité faible avec comme nom court `"="`. Par exemple :

`x = g(x,a)`

Dans cette instruction, la valeur calculée par `g(x,a)` est affecté à la variable `x`. Si la tranmission de données se fait par référence statique, on utilise un autre signe d'affectation :

`x ≝ g(x,a)`

Délors `x` fait référence au terme mais précisément celui qui est écrit là. Il y a donc une infomation supplémentaire que `x` possède, qui est le lieu où est écrit le terme en question dans le programme. Autrement dit la référence statique `x` pourrait servir de goto. La référence dynamique, quant à elle, ne s'applique pas à un terme, c'est un pointeur vers le premier pointeur d'une variable, sujet que nous verrons plutard.

Lorsqu'un opérateur normal reçoit comme argument une variable et un terme, il commence d'abord par les évaluer, il les remplace tout deux par leur valeur, et s'il n'arrive pas à les évaluer ni à s'évaluer lui-même alors il va retourner le terme qu'il engendre en remplaçant les arguments évaluables par leur valeur. Et de façon plus complète s'il passe par plusieurs niveaux d'évaluation, il engendre un ensemble de termes.

L'opérateur d'affectation se comporte différemment puisqu'il ne va pas remplacer son premier argument par sa valeur. L'opérateur d'affectation modifie le contenue de la variable transmis en premier argument. Une variable put contenir un programme. Dans le cas d'une affectation d'une réference statique, le deuxième argument n'est pas évalué puisque c'est seulement la référence au terme qui est transmise à la variable.

L'évaluation d'une telle variable référente peut se faire de deux façons :

Dans ce dernier cas la variable référente se comporte comme une variable normale (en économisant la mémoire).

Le teste d'égalité en valeur entre deux termes est l'équivalent passif de l'affectation `=`, c'est l'opérateur binaire de nom long `égal?` et de nom court `"=="` avec une syntaxe centrée et qui retourne vrai ou faux. Par exemple :

`f(x)"=="g(a,y)`

6) Variables locales

Les variables peuvent être déclarée localement c'est à dire dans un contexte ou un environnement dont la portée est limitée à un bloc de code entre accolade `{ }`, ainsi le langage s'y trouve augmenté d'une variable, et cette variable peut venir masquer une variable de même nom prééxistante définie dans un bloc parent. Les blocs de codes s'emboitent les uns dans les autres et forme un arbre. La portée des variables s'étend par héritage aux sous-blocs tant que le sous-bloc ne redéfinit pas une nouvelle variable de même nom auquel cas celle-ci masque et remplace la précédente.

Délors pour déclarer une variable dont le nom est éventuellement déjà pris, il nous faut utiliser un opérateur spécial var, où const, et si on souhaite que la variable local soit pérenne c'est-à-dire qu'elle soit global mais accessible que dans le bloc de code en question (et ceux descendants), on utilise l'opérateur spécial global var, global const. On remarquera que dans ce cas la variable n'est pas initialisée, qu'il faut donc prévoir une initialisation à la première exécution du bloc de code. Exemples :

var `x`

const `x`

global var `x = ...`  sachant que l'initialiation n'est faite qu'à la première exécution du bloc de code.

global const `x = ...`  sachant que l'initialiation est faite à la première exécution du bloc de code.

Chaque nouvelle variable mentionnée agrandit le langage. De tel sorte que la variable se comporte comme un terme nucléïque, à-part qu'elle peut changer de valeur si elle est déclarée avec var ou global var. Et nous sommes toujours dans un langage monotype, la variable, le terme et la valeur sont de même type. Déclarer une variable va ajouter un nouvel élément inconnu, sachant que cet élément peut être égal à un élément déjà connu ou non, son introduction a le même effet qu'une extension libre de structure que nous décrivons dans la partie 3. Mais avant d'explorer cette partie mathématique il convient de décrire l'autre face de la variable qu'est le pointeur, ce que nous ferons dans la partie 2. Puis il convient d'abord d'étudier les base de la programmation par le bas.

7) Bloc de code et opérateur

Une approche de la programmation des opérateurs peut être faite par le bas à travers la notion de bloc de code. On enrichit le langage pour pouvoir désigner une succession de termes en utilisant l'opérateur binaire point-virgule. Chaque terme est une instruction. L'opérateur binaire point-virgule désigne une succession série de termes, tandis que l'opérateur binaire virgule désigne une succession parallèle de termes. Le bloc de code est délimiter par des accolades, et est associé à un contexte. Le bloc de code représente un programme, et représente également un opérateur. Il doit donc pouvoir avoir plusieur arguments d'entrées qui sont listés en premier dans son code avant l'opérateur spécial |, et il doit pouvoir avoir plusieurs arguments de sortie qui sont transmis par l'opérateur return ou directement. Par exemple :

`{x,y | "instruction1"; "instruction2" ; ... ; "return"(a,b,c)}`

Ce bloc de code prend deux arguments en entrée et retourne 3 arguments. Autre exemple :

`{x,y | y,x}`

Ce bloc de code prend deux arguments en entrée et retourne les deux mêmes arguments mais dans l'ordre inverse. Le bloc de code peut contenir des variables statics qui se comportent comme des variables globals mais accessible uniquement à l'intérieur du bloc. Ce sont les variables déclarées avec global. L'appel d'un bloc de code se fait de la même manière que l'appel d'un opérateur appliqué à ses arguments. On mémorise le bloc de code dans une variable `B` à l'aide de l'opérateur d'affectation spéciale `≡` qui permet de mémoriser non-pas une valeur mais un terme. Puis on appelle le bloc de code avec ses arguments s'il en a :

`B ≡{x,y | "instruction1"; "instruction2" ; ... ; return(a,b,c)}`
`B(u,v)`

ou directement

`{x,y | "instruction1"; "instruction2" ; ... ; "return" (a,b,c)}(u,v)`

Ainsi le bloc de code constitue un opérateur. Cest le code source d'un programme qui peut être utilisé tel quel comme un opérateur, et donc, l'exécution d'une telle instruction va interpréter le bloc de code. Lorsque l'on affecte le bloc de code à la variable `B`, on ne le duplique pas, on transmet seulement une référence du bloc de code, de telle sorte que l'exécution de `B(u,v)` correspond à l'exécution du bloc de code là où il est déclaré.

8) Arité variable et logique polonaise

Les blocs de codes définissent des opérateurs d'arité diverse. Dans un langage monotype composé d'opérateurs d'arité divers, on est contraint de donner un sens à l'application d'un opérateur `(p,q)`-aire appliqué à `n` arguments. L'opérateur standart évalue les premiers arguments dont il a besoin en une listes de valeurs, avant de s'évaluer lui-même, sachant que l'évaluation d'un terme peut produire une liste de valeurs. Pour ne par perdre d'information les arguments et valeurs supplémentaires non utilisées par l'opérateur, les valeurs et arguments en trops sont concaténés au résultat. Ce mécanisme d'évaluation aboutit à la logique polonaise. Ainsi, étant données les opérateurs suivants, nous avons par exemples :

Opérateur Arité Description de l'arité
`a`
(0,1)
`|->"."`
`b`
(0,2)
`|->".,."`
`f`
(1,1)
`"."|->"."`
`g`
(2,1)
`".,."|->"."`
`m`
(1,2)
`"."|->".,."`

`f(a,b,a) "==" f(a),b,a`
`f(b,a,b) "==" f(b_1),b_2,a,b`
`a(b) "==" a,b`
`b(a) "==" b_1,b_2,a`
`b(b) "==" b_1,b_2,b`
`f(b,a) "=="  f(b_1),b_2,a`
`g(a) "=="  g(a,".")`
`g(b) "=="  g_1(b_1,b_2),g_2(b_1,b_2)`
`f(g(b)) "=="  f(g_1(b_1,b_2),g_2(b_1,b_2))`
`g(a,b,a) "=="  g_1(a,b_1),g_2(a,b_1),b_2, a`

Pour ne pas réduire ce mécanisme à la seule logique polonaise, on introduit un opérateur de regroupement `[...]` le plus générale qui soit, qui retourne le contenu d'une liste de valeurs sous forme d'une seule valeur sans perte d'information. Ainsi, nous avons par exemples :

`g([a,b],b,b) "==" g([a,b],b_1),b_2,b`
`(a) "==" a`
`[a] "==" a`
`(a,b) "==" a,b`
`[b] ≠ b`
`[b] "==" [b_1,b_2]`
`f([a,b],b) "==" f([a,b],),b`

Les parenthèses jouent alors de rôle de simple priorisation et donc définissent avec la virgule que des séquences applatie de valeurs une fois évaluées tandis que les crochet `["..."]` jouent alors de rôle de construction de liste de valeurs formant une valeurs.

La logique polonaise se base sur une granulation de valeurs. Et l'opérateur de constitution de liste, constitue simplement un opérateur particulier d'arité (variable,1) transformant une liste de valeurs en une seule valeurs mais sans pertes d'information.

Les variables sont aussi des opérateurs et peuvent être définie par un bloc de code. Par exemple :

`a={0x000F}`

Ce bloc de code ne comprend qu'une valeur de retour, un mot de 16 bits où les 4 bits de poid faibles sont à 1 et les autres bits sont à 0. On peut considérer ce bloc de code comme la version non-compilée de la variable `a`.

8) Espace de nom, objet et langage

L'ensemble des variables statics du bloc de code forme un espace de noms d'opérateurs complétant le langage, mais ces variables qui peuvent constituer des opérateurs ne sont appelables uniquements que dans le bloc en question, et sont initialisées seulement lors de la première exécution des instructions de leur déclaration.

On perfectionne la notion de bloc de code en lui spécifiant une méthode pour appeller ses opérateurs statics. On utilise l'opérateur spéciale point de syntaxe centrée qui appliqué à un bloc de code et à un nom de variable va désigner l'opérateur ayant ce nom qui est déclaré de manière static dans le bloc de code. Ainsi l'argument de droite du point est un nom appartenant à l'ensemble des noms de variables déclarées avec global dans le bloc de code.

Le bloc de code définit un langage dont la définition est l'ensemble des opérateurs déclarés avec global dans le bloc de code. À ce stade, un opérateur, une variable, un programme et un langage, désigne la même chose.

On lance le programme `g` du bloc `B` avec les arguments `u,v` comme suit :

`B.g(u,v)`

Si `g` est déclaré avec global dans le bloc de code `B`. Même si le langage parent possède déjà une définition de l'opérateur `g`, il n'y a pas d'ambiguïté car l'opérateur point impose le langage de l'opérateur de gauche, appelé objet ou aussi espace de nom, pour l'interprétation de son argument de droite.

A ce stade le langage est définie dans un programme et il constitue simplement un espace de noms et constitue un objet, un objet d'une classe singleton car il est son propre modèle.

L'initialisation de `B` se fait par l'affectation à `B` d'un bloc de code. Ce bloc de code est mémorisé dans `B` sans être exécuté. Puis après, les appelles sont de la fome `B.` suivie d'un nom de variable déclaré par global dans le bloc de code. L'initialisation des variables global se fait dans les déclarations global

`B={"global var" x = ...; "global var" y=....; ...}`

Cela crée un objet `B` possédant des attributs `x,y...` et qui peuvent constituer des méthodes.

On voit comment par généralisation successive et par unification des notions et des procédés on peut arriver à construire par étapes, les différents paradigmes programmatifs comtemporains. En continuant ainsi nous dévoilerons le principe de la programmation objet et sa forme d'implémentation.

9) L'instanciation statique

On perfectionne la notion de bloc de code en lui permettant plusieurs instanciations distinctes. Qu'est-ce donc que ce concept d'instanciation ? L'idée est de pouvoir démultiplier un même programme pour pouvoir faire en même temps plusieurs mêmes calculs avec des données différentes. Prenons par exemple le programme d'un compteur, `B` :

`B={`
    `"global var " x =0`
    `"global const " s = {x=x+1}`
`}`

L'exécution de `B` va initialiser la variable `B.x` à `0` et va mettre dans`B.s` une référence au bloc de code `{x=x+1}` sachant que le `x` en question désigne la variable `B.x`, puisque placé dans le bloc de code `B`. Le code n'est pas recopier dans B.s, seulement une référence est mémorisé dans B.s vers le

`B` constitue un programme de compteur qui va servir de modèle pour construire plusieurs programmes de compteur. La variable d'origine `B` est utilisé comme modèle que l'on instancialise `n` fois pour créer `n` objets à son image.

`u={"static var "x = 0 ; "static const " s = {x=x+1}}`
`v={"static var "x = 0 ; "static const " s = {x=x+1}}`
`w={"static var "x = 0 ; "static const " s = {x=x+1}}`
...

Puis il est dispendieux de vouloir réecrire `n` blocs de codes identiques. On définit un nouveau bloc grace aux crochets `{}`, et on y place à l'interieur une référence vers un autre bloc :

`u={"@"B}`
`v={"@"B}`
`w={"@"B}`

Le symbole `"@"` appliqué à un bloc retourne son contenu. Cela peut entrainer des doubles déclaration-initialisation d'une même variable, ce qui sera interprété comme des affectations successives qui ne s'appliquent qu'a la première lecture du bloc.

Puis il faudra initialiser ces nouveaux bloc de code qui sont des instanciations de `B` appelées objets :

`u;v;w`   // Initialise les 3 trois instanciations `u,v,w` du bloc de code `B`.

Les objets sont créé statiquement, car chaque tel objet correspond à un bloc de code présent forcement quelque part dans le programme.

10) Chimères

Considérons un second programme de compteur :

`C={`
    `"static var " y`
    `"static const " p = {y=y"*"2}`
`}`

Il est possible de concaténer les deux programmes :

`BuuC=={`
    `"static var " x`
    `"static const " s = {x=x+1}`
    `"static var " y`
    `"static const " p = {y=y"*"2}`
`}`

Et d'en créer une copie statique :

`u={"@"B;"@"C}`
`u` 
  // Initialise l'instanciation `u` du bloc de code `BuuC`.

11) Paramètres d'initialisation

Lorsque il y a des paramètres par exemple `a,b` pour l'initialisation du bloc, le bloc les prend comme arguments

`B={a,b "|"`
    `"static var " x=a`
    `"static const " s = {x=x+b}`
`}`

Dans cette exemple, `a` désigne le numéro de départ du compteur et `b` désigne le pas d'avancement du compteur. L'instanciation avec paramètres `a,b` reprend le code de `B` en substituant `a` et `b` par leur valeur. En fin de compte, on réitère simplement la même forme :

`u={a,b|"@"B}`
`u(".,.")`    // Initialise l'instanciation `u` du bloc de code `B`.

12) L'instanciation dynamique

L'instanciation dynamique crée une forme compilé d'objet, et sera décrite après la notion de compilation.


Dominique. Mabboux-Stromberg (Décembre 2023)

 

Suivant