Précédent
 
Suivant

Logique

(Partie 2)

 

1) La machine vue comme interface

Nous voulons formaliser les premières bribes d'un langage informatique permettant de définir les machines par une approche globale, orientée ici comme interface. C'est-à-dire au lieu de chercher les procédés effectifs élémentaires que peut faire une machine et percevoir toutes les machines que l'on peut construire avec, on procède à une analyse réactionnelle de la machine. Comment intéragit-elle avec son environnement ? . Qu'est-ce que produit une machine ? Qu'est-ce qu'on peut faire avec une machine ? Comment la machine intéragie avec l'utilisateur, comment peut-elle intéragire avec une autre machine ? etc..

On s'immisce dans le fonctionnement de la machine, en programmant son interface, régissant les relations entre elle et son environnement. Et on le fait en établissant des programmes séquentiels d'exécution élémentaire, utilisant la notion de machine comme notion de sous-programme, un même motif répété comme dans un fractal, traduisant les procédés effectifs mis en oeuvre par la machine sans les définir. Seuls sont définie les procédés effectifs élémentaires de l'interface.

Le langage de programmation sera obtenu par une analyse descendante de l'interface partant du générale pour aller au particulier. Lançons-nous donc dans la conception d'un tel langage. Il intégrera les connaissances programmatives d'aujourd'hui en étant orienté objet, fonctionnelle, et orienté aspect. Il apparaitra comme une sorte d'artéfact d'un langage tel que Ruby.

2) Les base théoriques du raisonnement

Le raisonnement pour être exacte nécessite un langage formel, noté `L`, pour d'écrire l'état du système procédant au raisonnement, et qui jouera le rôle de carrière, et qui constituera l'ensemble des propositions exprimables. Ce langage doit être énuméré par une machine souche que l'on nomme de la même façon `L` par simplicité.

Puis on conçoit une seconde machine dite démonstrative qui va définir un langage `M` inclus dans `L`, que sont les déductions. On commence ainsi en situation prélogique où il n'y a pas de négation et donc où il n'y a pas de réponse négative ni d'état rejetant. Mais il peut y avoir une absence de réponse correspondant à un calcul sans fin ne produisant rien.

La machine démonstrative `M` énumère l'enemble des propositions tautologiques qui forment un langage que l'on nomme de la même façon `M` par simplicité.

`M sube L`

Etant donnée une proposition `x` appartenant à `L`. On dira que `M` énumère `x`, ou que `M` reconnait `x`, ou que `M` démontre `x` lorsque dans l'énumération sans fin des productions de `M`, à un instant donné `M` produit `x`, et on notera cette propriété mathématique par l'expression suivante :

`M⊢ x`

Par définition, cette proposition est équivalente à ce que `x` appartienne au langage `M` :

`x in M`

La machine `M` est un énumérateur. Elle énumère une listes de propositions sans fin. Et en ce plaçant à l'infini des temps..., deux cas peuvent se produirent :

 

3) Enumérateur

L'énumérateur `M` possède plusieur forme effective. Mais ce qui est important c'est de choisir une forme traduisant de façon exacte l'essentiel du procédé effectif de l'énumérateur. On représente l'énumérateur comme un petit système. On lui adjoint un attribut privé `n "∈" NN` qui est un compteur, que l'on note `M"."n` en reprenant la syntaxe utilisée dans de nombreux langages de programmation pour désigner l'attribut `n` d'un objet `M`. Mais il est privé car on ne peut pas le modifier de l'extérieur, on peut juste le lire, et seul `M` le modifit lui-même lors de l'exécution de la méthode `"≫"` calculant l'élément suivant dans l'énumération qu'il est sensé produire. En procédant ainsi, on ne nuit nullement à la généralité, on lui adjoint simplement un artifice de complexité rudimentaire qui nous permet de dévolopper l'interface de la machine.

Et on lui adjoint une méthode `sf"init"()` pour l'initialiser. L'appel sans argument, indiqué par le suffix `()`, précise que c'est une méthode et non un attribut. La particularité d'une méthode et que son exécution est susceptible de modifer l'état de l'objet, et d'autre part une méthode peut posséder des arguments d'appel et correspond à un opérateur d'un type définie. L'instruction initialisant l'énumérateur est `M"."sf"init"()` selon la syntaxe orienté objet. On ajoute alors un sucre syntaxique pour permettre l'écriture `sf"init"(M)`. Cela signifie que lors d'un tel appel, le nom de la fonction `sf"init"` est rechercher entre-autre parmis les méthodes sans argument de `M`.

Les premières instructions qui apparaîssent pertinentes sont ; la mise à l'état initiale de la machine, puis l'exécution d'une étape de calcul produisant une proposition.

`sf"init"(M)`
`M"≫"x`

L'opérateur `"≫"` s'applique à un objet `M` ayant les qualités d'une pile. Il va procéder au calcul de la production de `M` en modifiant `M` et en incrémentant l'attribut `n` de `M`, puis va remettre le résultat dans `x`.

On remarque alors, que l'initialisation de `M` consiste à recopier dans `M` l'état initiale de la machine. Aussi, il n'est pas nécessaire d'en faire une méthode. On peut seulement vouloir garder en mémoire la machine en son état initial `M_0`. L'initialisation de la machine se fera par une instruction d'assignation :

`M"="M_0`

L'égalité correspond ici à une assignation, remplaçant le contenue de la variable `M` par le contenue de la variable `M_0`.

4) Le typage par inférence

La lourdeur de nombreux langage de programmation tient dans l'exigence de rappeler le type et la syntaxe de chaque opérateur, lorsqu'il n'y a pas de mécanisme pour le déterminer automatiquement. On souhaite définir un langage le moins verbeux possible, tout en restant dans le cadre d'une simplification et d'une vulgate propres à l'interface homme-machine. La première approche consiste à exploiter le langage terminologique multitype. On dispose d'opérateurs pouvant posséder plusieurs syntaxes et plusieurs types. On choisie comme syntaxe de référence la syntaxe infixe avec parenthèse. Considérons un opérateur `f` et des opérateurs nullaires `x,y`. Voici les différentes syntaxes, appelées aussi les différents rôles, que peut prendre `f` :

  1. Opérateur nullaire, `f=f`
  2. Opérateur unaire infixe, `f(x)=f x`
  3. Opérateur unaire postfixe, `f(x)=x f`
  4. Opérateur binaire infixe, `f(x,y)=f x y`
  5. Opérateur binaire postfixe, `f(x,y) = x y f`
  6. Opérateur binaire centré, `f(x,y) = x f y`

l'opérateur `f` peut cumuler plusieurs rôles avec des priorités syntaxiques fixées arbitrairement. Ainsi l'expression `f g h` selon le rôle possible de chaque opérateur parmi `1,2,3,4,5,6`, et selon leur priorité syntaxique, peut produire 11 compositions différentes :

  `f g h`
`f`
`g`
`h`
  `f(g,h)`
6
1
1
  `f(g(h))`
2
2
1
  `f(h(g))`
2
1
3
  `f(g)(h)`
2
1
1
  `g(f,h)`
1
6
1
  `g(f)(h)`
1
3
1
  `g(h)(f)`
1
2
1
  `h(f,g)`
1
1
6
  `h(g(f))`
1
3
3
  `h(f(g))`
2
1
3
  `h(g)(f)`
1
1
3

On intègre la syntaxe dans le type puisque celui-ci est sensé spécifier le comportement de l'opérateur. L'opérateur `f` d'une syntaxe devrait pouvoir ainsi se distinguer de l'opérateur `f` d'une autre syntaxe. Mais toutes ces compositions ne peuvent pas être rendues égales en n'utilisant le seul concept de currification. On réduit le nombre de type syntaxique à la seule arité. Et pour chaque arité, une syntaxe plus précise doit être déclarée si elle diffère de celle par défaut qui est la syntaxe infixe avec parenthèse. Par exemple l'opérateur `-` d'arité 1 pourra être défini en même temps que l'opérateur `-` d'arité 2, en spécifiant leur syntaxes spécifiques.

On peut encore réduire cette verbosité. La syntaxe par défaut utilisant toujours des paratenthèses sauf pour les opérateurs nullaires élémentaires, une seconde syntaxe par défaut peut être définie, qui sera une syntaxe sans parenthèse par défaut.

Arité
Première syntaxe
par défaut
Seconde syntaxe
par défaut
1
 `lambda(x)` 
 `lambda x` 
2
 `lambda(x,y)` 
 `x lambda y` 
3
 `lambda(x,y,z)` 
 `x lambda y lambda z` 

Puis il convient de préciser comment on sépare les noms des opérateurs. Cela se fait soit en utilisant un caractère blanc comme séparateur, ou soit en juxtaposant deux caractères incompatibles pour former un nom d'opérateurs. Ce dernier point se décline en une série de règles suivantes. On considère différents sous-ensemble de caractères :

Chiffre : `0123456789`
Lettre : `abcd...ABCD...`
Droit : `"abcd...ABCD..."`
Gras : `bb"abcd...ABCD..."`
Jambage : `bbb"ABCD"`
Gothique : `fr"ABCD"`
Grec : `alpha beta gamma ...`
Ponctuation : `".,!?"`"`"§';:"`
Opération : `"+-=><~#&|¦@°"`
Parenthèse : (){}[]

Les caractères Parenthèse, Ponctuation, Grec, Gothique, et Jambage sont solitaires, c'est à dire qu'ils désigne un nom d'opérateur d'un seul caractère qui peut être complété par un nom en indice.

 

La définition d'un opérateur ne doit être que la généralisation de la définition d'un opérateur nullaire. C'est pourquoi on commence par circonscrire ce qu'est la définiton d'un opérateur nullaire. L'opérateur nullaire n'est qu'une variable. Il possède un nom et un contenue d'un type donnée.

 

L'instruction de définition d'un nouvel opérateur

 

 

---- 28 janvier 2023 ----

 

 

 

 

Par exemple considérons les instructions suivantes :

`y=f(x)`
`z"="x"*"y`

Les seuls bases pour l'instant que nous posons sont :

Les types vont se construire par inférence. Dans la première instruction l'interpréteur reconnaitra trois opérateurs `y,f,x`. Au départ le type d'un nouvel opérateur est vierge et il accumule différent rôles que l'on rencontre. `y` joue le role d'opérateur nullaire.`f` joue le role d'opérateur unaire. Et `x` joue le rôle d'opérateur nullaire. Dans la seconde instruction l'interpréteur reconnaitra 4 opérateurs `z, x,"*",y` grace à des règles de séquençage de mots selon le type de caractère que nous détaillerons plus loin. Et il n'y a pas de différence de nature en dehors de leur type déterminé par inférence entre les opérateurs `x` et `"*"`, ce sont deux variables au même capacité sémantique. Et l'assemblage `"x"*"y` entraine beaucoup de possibilité de nouveaux rôles :

`x` opérateur binaire unfixe, et `*` et `y` opérateurs nullaires.

`x` opérateur unaire unfixe, et `*` et `y` opérateurs nullaires.

 

 

 

 

L'expression `f(x)` est un appel de la fonction `f` avec l'argument `x`. La priorité syntaxique de l'appel unfixe avec parenthèse est posée comme étant la plus forte. Ainsi `x,y,z` joue le rôle d'opérateurs nullaires puisqu'à certain endroit ils n'ont pas d'argument, ce qui est représenté par le type `Omega`. Autrement dit, ils sont autorisé à se comporter comme des éléments. Et cela n'exclue pas qu'ils aient d'autres rôles à prendre.

`sf"type"(x) = Omega`
`sf"type"(y) = Omega`
`sf"type"(z) = Omega`

Dans la partie droite de la deuxième instruction, l'interpréteur reconnait d'abord 3 opérateurs `x, "*", y` grace à des règles de séquençage de mots, puis il envisagera toutes les compositions possibles, à savoir  que l'opérateur `"*"` peut jouer soit le rôle d'opérateur binaire avec une syntaxe centrée, soit le rôle d'opérateur unaire infixe produisant un opérateur unaire postfixe ou inversement. Et en faite, il jouera les trois rôles.

`sf"type"(f) = f(Omega)`
`sf"type"("*") = Omega × Omega --> Omega`

`sf"type"("*") = Omega --> Omega --> Omega`

Si l'opérateur est déjà prédéfini, sont type et sa syntaxe sont alors déjà prédéfinis. L'instruction redéfinit l'opérateur mais ne redéfinit pas son type ni sa syntaxe, il peut les compléter, mais pas les réduire. Il faut donc une instruction spécifique pour supprimer le type d'un opérateur et sa syntaxe que l'on intègre au type, et que l'on plonge dans une instruction plus générale :

`sf"type"("≫")="nil"`

 

---- 28 janvier 2023 ----

 

 

Si aucun type n'est prédéfinie, les instructions du bloc de code construisent les types par inférence. Ainsi `M` est nécessairement un objet qui possède un attribut `n`, et deux méthodes `sf"init"` et `sf"read"` (inernes ou externes). Si le type de `"++"` précise que c'est un opérateur unaire postfixe intitulé incrémentation, alors l'attribut `n` possède une méthode d'incrémentation `"++"` de syntaxe postfixe. Puis `x` est de type résultat de l'application `sf"read"` appliqué à `M`. Ainsi, il n'est plus nécessaire de spécifier la syntaxe ni même le type.

On fixe la priorité syntaxique de l'égalité, comme étant la plus faible. L'expression suivante va définir l'opérateur avec sa syntaxe.

 

 

 

`sf"init"(M)`
`x"="sf"read"(M)`
`M"."n"++"`

`"++"` est un opérateur unaire de syntaxe postfixe. Il incrémente l'objet sur lequel il s'applique. Les égalités correspondent ici à des assignations. Mais ces deux dernières instructions ne doivent pas permettrent que d'autres instructions parallèles concernant leur champs d'action puissent s'exécuter entre eux deux. C'est à dire, à partir du moment où j'ai modifié `x` et jusqu'au moment où j'ai modifier `M.n` aucune action ne doit modifier `x`. Et d'autre part, l'attribut `M.n` ne doit pas être modifié d'une autre façon, car il traduit l'état de `M` après le calcul d'une production. Ces contraintes sont difficiles à préciser dans le langage de programmation. Nous verrons comment cela prendra forme.

Il existe un opérateur qui ne fait que calculer `x` sans le dépiler, et qui remet l'état `M` en l'état antérieur, et que l'on note `sf"read"(M)`. Mais il est inutile de calculer deux fois la même choses, c'est pourquoi on procède pour la même façon que le compteur n, en créant un attribut m qui contiendra la valeur calculé par `sf"read"(M)` si par hasard cette instruction a été exécuté, et qui contiendra `"nil"` sinon. Et donc qu'il faudra remettre à nil de manière immédiate comme précédement lorsque l'on calculera l'états suivant de `M`.

Ainsi l'opérateur read peut se définir à partir de l'opérateur de dépilement comme suit :

`sf"read"={M"|" A=M; `M"≫"x`; M=A}`

Et réciproqement l'opérateur de dépilement peut se définir à partir de l'opérateur read comme suit :

`"≫"={M,x "|" x"="sf"read"(M); M"."n"++"}`

 

 

L'opérateur `"≫"` est une procédure comprenant deux arguments d'appel `(M,x)`, et se définit par un sous programme mis sous forme de bloc de code. Et on reprend la notation de Ruby qui entoure le bloc de code d'acolades `{}`, énumère les arguments d'appel en premier jusqu'au symbole `|`, puis énumère les instructions impératives séquentielles séparées par des point-virgules. On ajoute alors un sucre syntaxique pour permettre l'écriture `"≫"(M,x)={...}`. Cela signifie que lors d'un tel assignement définitionnel de procédure, les noms des arguments d'appel utilisés dans le bloc de code sont rechercher dans l'appel définitionnel du membre de gauche.

`"≫"(M,x) = {x"="sf"read"(M); M"."n"++"}`

Par défaut, la syntaxe d'un opérateur est infixe. Dans le cas d'un opérateur binaire, pour rendre sa syntaxe centrée et de priorité `p`, on executera une instruction modifiant le langage comme suit :

`sf"Syntaxe"("≫","''centre''",p)`

Néamoins, on sent là une lourdeur dû à la nécessité de rappeller le type et la syntaxe de chaque opérateur, lorsqu'il n'y a pas de mécanisme pour le déterminer. On souhaite définir un langage le moins verbeux qu'il soit possible d'obtenir dans le cadre de la simplification effective et d'une vulgate propre à l'interface homme-machine.

Si aucun type n'est prédéfinie, les instructions du bloc de code construisent les types par inférence. Ainsi `M` est nécessairement un objet qui possède un attribut `n`, et deux méthodes `sf"init"` et `sf"read"` (inernes ou externes). Si le type de `"++"` précise que c'est un opérateur unaire postfixe intitulé incrémentation, alors l'attribut `n` possède une méthode d'incrémentation `"++"` de syntaxe postfixe. Puis `x` est de type résultat de l'application `sf"read"` appliqué à `M`. Ainsi, il n'est plus nécessaire de spécifier la syntaxe ni même le type.

On fixe la priorité syntaxique de l'égalité, comme étant la plus faible. L'expression suivante va définir l'opérateur avec sa syntaxe.

`M"≫"x = {x"="sf"read"(M); M"."n"++"}`

L'interpréteur reconnaitra d'abord 3 opérateurs `M, "≫", x` grace à des règles de séquençage de mots, puis il déterminera que `x` joue le rôle d'opérateur nullaire car `x"="...` , puis que `M` joue le rôle d'un opérateur nullaire car `sf"read"(M)`, et donc l'expression à gauche de l'égalité `M"≫"x` n'a de sens que si l'opérateur `"≫"` peut jouer soit le rôle d'opérateur binaire avec une syntaxe centrée, soit le rôle d'opérateur unaire infixe produisant un opérateur unaire postfixe ou inversement. Et en faite, il jouera les trois rôles.

Si l'opérateur est déjà prédéfini, sont type et sa syntaxe sont alors déjà prédéfinis. L'instruction redéfinit l'opérateur mais ne redéfinit pas son type ni sa syntaxe, il peut les compléter, mais pas les réduire. Il faut donc une instruction spécifique pour supprimer le type d'un opérateur et sa syntaxe que l'on intègre au type, et que l'on plonge dans une instruction plus générale :

`sf"type"("≫")="nil"`

3) Enumérateur et prédicat

Il existe deux formes effectives pour définir un langage, que sont l'énumérateur et le prédicat. Les énumérateurs énumèrent tous les mots du langage. Les prédicats prennent comme argument un mot et lance un calcul. Si le calcul s'arrête alors le mots fait partie du langage. Et si le calcul ne s'arrète jamais alors le mot ne fait pas partie du langage.

On passe de l'un à l'autre par un procédé effectif qui doit pouvoir se programmer simplement dans notre langage de programmation.

 

---- 28 janvier 2023 ----

 

 

 

M.n

qui est un entier et qui indique l'ordre de l'élément qu'il est entrain de calculer, ou, ce qui revient au même, le nombre d'élément qu'il a déjà énuméré.

que l'on peut mettre sous forme d'une application que l'on nomme pareillement `M`.

`M ∈ (NN→(Luu{oo}))`

`oo` est l'infinit de Turing c'est à dire un calcul qui ne s'arrête pas, et donc qui ne donne pas de réponse. Il y a alors deux sortes d'énumérateurs, ceux qui énumère un nombre infini d'éléments : `(M(0), M(1),M(2),...)` et ceux qui énumère qu'un nombe fini d'éléments : `(M(0),M(1),M(2),..., M(n),oo)`. On constate la règle suivante, si `M(n)=oo` alors `M(n"+"1)=oo`, ou par contraposé si `M(n"+"1) in L` alors `M(n) in L`. Ansi l'expression `M(3)"="x` signifit que `M` va d'abord énumérer les propositions `M(0), M(1), M(2)`, puis la proposition suivant énénumérée par `M` sera `x`.

Nous avons ainsi formaliser les premières bribes d'un langage mathématique permettant de définir des propriétés rudimentaires sur les machines. Mais on souhaite s'immiscer dans le fonctionnement de la machine, et établir un programme séquentiel d'exécution, traduisant les procédés effectifs nécessairement mis en oeuvre par la machine. On obtiendra ainsi un langage de programmation défini par une analyse descendante partant du générale pour aller au particulier. Lançons-nous donc dans la mise au point d'un tel langage. Il sera orienté objet, fonctionnelle, aspect, et apparaitra comme une sorte d'artéfact du langage Ruby.

 

 

 

Il y a deux modes de fonctionnement d'une machine démonstrative, le mode énumératif et le mode prédicatif. Les énumérateurs énumèrent tous les mots du langage. Les prédicats prennent comme argument un mot et lance un calcul. Si le calcul s'arrête alors le mots fait partie du langage. Et si le calcul ne s'arrète jamais alors le mot ne fait pas partie du langage.

Les premières instructions qui apparaîssent pertinentes sont la mise à l'état initiale de la machine, puis l'exécution d'une étape de calcul produisant une proposition.

`sf"init"(M)`
`M"⊢" x`

La machine `M` possède une méthode `sf"init"()` qui initialise la machine. Dans un langage de programmation orienté object, la commande s'écrit `M"."sf"init"()`. Mais par soucis pratique, on définit une procédure globale `sf"init(.)"` qui prend comme argument une machine, et qui l'initialise. Cela se programme comme suit :

`sf"init"={M "|" M"."sf"init"}`

Voyez comment la procédure globale `sf"init"` est programmer par un bloque de code `{M "|" ...}` dont la première partie liste les arguments d'entrée et la seconde partie décrit le procédé effectif mis en oeuvre.

L'instruction `M"⊢" x` n'a pas le même sense que la propriété `M"⊢" x`. En tant qu'instruction, la machine `M` possède une méthode `"⊢"` unaire qui prend comme argument une variable réceptrice `x` qui joue le rôle d'une variable ici implicitement déclarée par inférence de type, et donc qui ne doit pas être évaluée, et qui récupère la proposition produite par la machine aucours d'une étape de calcul. Dans un langage de programmation orienté object, la commande s'écrit `x=M".⊢()"`. Mais par soucis pratique, on définit une fonction globale `"⊢(.,.)"` de syntaxe centrée qui prend comme argument une machine et une variable réceptrice ne devant pas être évaluée, qui recoit la proposition produite par la machine. Cela se programme comme suit :

`"⊢"={M, &x "|" x=M".⊢()"}`

Le préfixe & devant l'argument `x` signifie que l'argument `x` ne sera pas évalué lors de l'appel, mais transmis comme variable.

L'état de la machine `M` possède une caractéristique définissable par en haut qui est le nombre d'étapes déjà exécutés, où l'on entend par étape, un calcul produisant une proposition. Cet attribut de la machine se note `e`. Ainsi `M"."e` désigne le nombre de propositions déjà calculées par `M`.

Le premier programme pertinent qui apparaît est celui d'une machine `K` qui énumère le même langage que celui de `M` mais qui l'énumère de façon biunivoque c'est-à-dire sans jamais se répéter.

`K"."sf"init" = {`
     `K"."e=0`
     `K"."p = []`
     `sf"init"(M)`
`}`

`K".⊢" = {&x "|"`
     `sf"for" {`
          `M"⊢" x`
          `sf"if" x"∉"p sf"then break "`
     `}`
     `x"≫"K"."p`
`}`

L'instruction `K"."e=0` crée l'attribut `e` pour la simili-machine `K` et l'initialise à la valeur `0`. L'instruction `K"."p = []` crée l'attribut `p` pour la simili-machine `K` et l'initialise à la liste vide `[]`. L'instruction `x"∉"p` teste si `x` n'est pas dans la liste `p`. L'instruction `x"≫"K"."p` empile `x` dans la liste `p`. L'objet `K` ainsi programmé se comporte comme une machine pour les méthodes découverte jusqu'à présent.

Pour interroger si `x` est reconnue par `M`, on écrira l'instruction suivante :

`x"∉"M`

La machine `M` désigne également l'ensemble énuméré par elle. L'opérateur `"∈"` diffère selon le type de `M` et se détermine donc selon son deuxième argument :

`"∉" ={x, M  "|" M".∉"(x)}`

`M".∉" = {x "|"`
     `sf"for" {`
          `M"⊢"y`
          `sf"if" x"=="y sf"then return" 1`
     `}`
`}`

`p".∉" = {x "|"`
     `sf"for" {`
          `sf"if" p"=="[] sf"then return" 0`
          `p"⊢"y`
          `sf"if" x"=="y sf"then return" 1`
     `}`
`}`

Qu'est-ce qui différencie `p` de `M`, c'est leur type qui peut être décrit par un attribut `sf"type"`, qu'il faut donc rajouter lors de leur création.

---- 8 janvier 2017 ----

 

Précédent
 
Suivant

 


Dominique Mabboux-Stromberg