• Aucun résultat trouvé

Chapitre 4 ContextAA – Modèle théorique

4.1 Vocabulaire de base

4.1.2 Opération primitive

Une opération est considérée primitive si elle peut être tenue pour acquise sans devoir être décrite ici dans le détail. Par exemple, la concaténation de chaînes de caractères est primitive au sens entendu ici, tout comme l’est l’addition d’entiers. En pratique, bien sûr, ces opérations ne sont pas nécessairement simples ou évidentes, mais il est présumé que les tenir pour acquises ne posera pas obstacle à la compréhension du texte.

4.2 Conventions et notation

Un certain nombre de conventions sont appliquées dans ce chapitre. Ce qui suit en explique les conventions et les a priori qu’elles impliquent.

4.2.1 Considérations générales

Les règles lexicales et grammaticales du Contexte tel que le définit ContextAA sont présentées à l’annexe A.

Nous utilisons le terme « standard », décrivant par exemple des Agents standards, des critères standards ou des algorithmes standards. Ce faisant, nous ne faisons pas référence à un standard dont serait responsable un organisme tel qu’OMG ou ISO mais bien à des outils standards à l’intérieur de ContextAA, donc qui y sont considérés comme des a priori systémiques.

Nous écrivons (𝑎, 𝑏, … , 𝑐) pour lister les symboles 𝑎, 𝑏, … , 𝑐. Cette notation servira aussi pour lister les paramètres formels d’une fonction (§4.2.8).

Nous écrirons [𝑎. . 𝑏] pour décrire un intervalle continu de valeurs allant de 𝑎 à 𝑏 inclusivement. Les crochets [ et ] interviendront aussi dans les notations de tableaux et de listes (§4.2.7); le sens à leur attribuer devrait être clair à partir de leur contexte d’utilisation.

Pour un ensemble 𝑆, l’écriture |𝑆| signifie « cardinalité de 𝑆 ». Pour un nombre 𝑥, l’écrire |𝑥| signifie « valeur absolue de 𝑥 ».

Nous écrivons Contexte, Agent et Hôte lorsque nous mettons de l’avant leur acception technique, distinguant cette écriture de celle que nous utilisons pour ces termes dans leur acception générale, soit contexte, agent et hôte.

76

Nous utilisons les néologismes « contextifier » (verbe) ou « contextification » pour exprimer la transformation d’une donnée d’un certain type en son équivalent sous forme de Contexte. Une entité qui peut être représentée sous forme de Contexte, donc contextifiée, est dite « contextifiable ». Un Agent du domaine (§Erreur ! Source du renvoi introuvable.) est c ontextifiable, et sa contrepartie exprimée sous forme de Contexte est contextifiée.

À l’inverse, nous utilisons le verbe « réifier » pour décrire la reconstitution d’un objet à partir de sa forme contextifiée. Reconstruire un Agent à partir de sa forme contextifiée est une réification de ces Agents.

Le passage du temps se représente par étapes discrètes dans ContextAA. Ainsi, lorsque nous exprimons une valeur de temps 𝑡 telle que l’indice d’une étape dans l’exécution d’un Hôte, nous considérons 𝑡 ∈ ℤ.

4.2.2 Appartenance à un type

Nous écrivons 𝑥: 𝑇 pour exprimer que 𝑥 est une instance du type 𝑇.

Nous considérons certains types comme primitifs, par exemple 𝑠𝑡𝑟𝑖𝑛𝑔 pour des chaînes de caractères, 𝑖𝑛𝑡 pour des entiers ou 𝑏𝑜𝑜𝑙 pour des booléens, sans définir systématiquement les bornes minimale et maximale qui leur sont associées.

Nous acceptons à titre de type un intervalle tel que [0. .1], signifiant « toutes les valeurs entre 0 et 1 inclusivement » ou (0,1), signifiant les seules valeurs 0 et 1. Nous acceptons aussi les notations mathématiques usitées telles que ℕ pour les entiers naturels ou ℚ pour les rationnels, sans bien sûr négliger la différence entre l’idéal mathématique d’un ensemble de cardinalité infinie et notre implémentation, plus concrète. Une écriture comme [0. .1] ∈ ℚ signifiera un rationnel situé inclusivement entre 0 et 1, et il en sera de même pour l’écriture 𝑟 ∈ ℚ, 0 ≤ 𝑟 ≤ 1.

Certains types sont particulièrement importants pour ContextAA. Entre autres, parmi ceux-ci, on trouve les noms (type 𝑁𝑎𝑚𝑒), les Contextes (type 𝐶), les Agents (type 𝐴), les Hôtes (type 𝐻) et les critères (type 𝐶𝑟𝑖𝑡). D’autres types clés de notre modèle sont introduits ultérieurement.

Lorsqu’un type est générique, nous utilisons une notation entre chevrons. Nous écrivons par exemple 𝑆𝑒𝑡〈𝑇〉 pour un ensemble générique d’éléments de type 𝑇 ou 𝐿𝑖𝑠𝑡〈𝑏𝑜𝑜𝑙〉 pour une liste de booléens.

4.2.3 Types de fonctions

Le type 𝐹𝑢𝑛𝑐 est utilisé pour identifier une fonction, sans égard à son arité.

Nous exprimons 𝑑𝑒𝑐𝑙𝑡𝑦𝑝𝑒(𝐹(𝑇)) le type du résultat de l’application d’une fonction de type 𝐹 sur une donnée de type 𝑇, tout comme nous exprimons 𝑑𝑒𝑐𝑙𝑡𝑦𝑝𝑒(𝐹(𝑇0, 𝑇1, … , 𝑇𝑛−1)) le type de l’application d’une fonction de type 𝐹 sur des paramètres de type 𝑇0, 𝑇1, … , 𝑇𝑛−1.

4.2.4 Foncteurs et expressions λ

Nous utilisons le vocable foncteur au sens d’un objet qui se comporte comme une fonction. En ceci, nous faisons un choix terminologique semblable à celui de C++, avec les Function Objects, et cousin de celui de certains langages fonctionnels où un foncteur est une fonction qui se comporte comme un objet. Les expressions λ sont pour nous une forme abrégée de foncteur anonyme.

77

Nous avons recours à un foncteur lorsqu’une opération bénéficie d’être porteuse d’états persistant d’un appel à l’autre, ce qui permet par exemple de construire une fermeture sur une valeur.

4.2.5 Chaînes de caractères

Nous considérons primitives les opérations telles que la concaténation ou la comparaison pour fins d’égalité de contenu ou d’ordonnancement lexicographique sur des chaînes de caractères. Nous considérons aussi, pour alléger le discours, que la conversion d’un nombre en chaîne de caractères est implicite lorsque le contexte le demande.

Par souci de simplicité, nous escamotons en grande partie la question des encodages de caractères pour fins d’internationalisation, et nous ne nous préoccuperons pas de majuscules et de minuscules. Évidemment, sur le plan technique, ContextAA traite ces considérations.

Nous écrivons une chaîne de caractères vide sous la forme "".

La fonction 𝑙𝑒𝑛𝑔𝑡ℎ de signature 𝑙𝑒𝑛𝑔𝑡ℎ ∷ 𝑠𝑡𝑟𝑖𝑛𝑔 → ℕ exprime le nombre de symboles d’une chaîne de caractères; ainsi, 𝑙𝑒𝑛𝑔𝑡ℎ("𝑎𝑏𝑐") = 3 et 𝑙𝑒𝑛𝑔𝑡ℎ("") = 0. Nous considérons cette fonction primitive.

La concaténation de chaînes de caractères s’exprime comme suit : 𝑐𝑜𝑛𝑐𝑎𝑡 ∷ 𝑠𝑡𝑟𝑖𝑛𝑔 → 𝑠𝑡𝑟𝑖𝑛𝑔 → 𝑠𝑡𝑟𝑖𝑛𝑔

𝑐𝑜𝑛𝑐𝑎𝑡("", "") ≜ "" 𝑐𝑜𝑛𝑐𝑎𝑡("", 𝑠) ≜ 𝑠 𝑐𝑜𝑛𝑐𝑎𝑡(𝑠, "") ≜ 𝑠

𝑐𝑜𝑛𝑐𝑎𝑡("𝑎 … 𝑏", "𝑐 … 𝑑") ≜ "𝑎 … 𝑑"

Nous présumons que la gestion de caractères spéciaux comme le caractère "\"" respecte les usages et les attentes de plusieurs langages de programmation, par exemple C, C++, Java et C#. En pratique, concaténer "J'aime mon \"" et "prof\"!" résultera en "J'aime mon \"prof\"!".

4.2.6 Ensembles

Nous écrivons {𝑎, 𝑏, … , 𝑖, … } l’ensemble contenant les éléments 𝑎, 𝑏, … , 𝑖, … Nous exprimons l’ensemble vide par ∅ ou { } selon les circonstances.

Dans ContextAA, un ensemble est typé : on parlera généralement de 𝑆𝑒𝑡〈𝑇〉 plutôt que de 𝑆𝑒𝑡 pour un ensemble dont les éléments sont de type 𝑇.

Étant donné un ensemble 𝑠 dans ContextAA, chaque élément de 𝑠 est unique dans 𝑠 : 𝑠: 𝑆𝑒𝑡〈𝑇〉, 𝑒: 𝑇 (𝑒 ∈ 𝑠 ⇔ (𝑠 ∪ {𝑒} = 𝑠))

Soit 𝑠: 𝑆𝑒𝑡〈𝑇〉, nous exprimons la cardinalité de 𝑠 par |𝑠|.

L’appartenance d’un élément 𝑒 à un ensemble 𝑆 s’exprime 𝑒 ∈ 𝑆 mais nous utilisons parfois 𝑒: 𝑆, à la manière de l’appartenance à un type, lorsque cela s’y prête et n’entraîne pas de confusion.

78

4.2.7 Listes

Nous exprimons une liste de 𝑛 éléments de même type sous la forme [𝑒0, 𝑒1, … , 𝑒𝑛−1] lorsque nous identifions les éléments un à un.

Lorsque nous souhaitons exprimer une liste par construction d’une tête 𝑡 et d’une queue 𝑞, nous exprimons par [𝑡|𝑞] la liste concaténée dont 𝑡 est la tête et 𝑞 est la queue; dans ce cas, 𝑞 est la liste des éléments à la suite de 𝑡. Pour exprimer une liste d’au moins un élément, nous écrivons [𝑡| … ]. La liste vide est représentée par [ ]. Il arrive que nous considérions comme interchangeables liste vide [ ] et ensemble vide ∅ lorsque le contexte s’y prête et lorsque cela permet d’alléger l’écriture.

4.2.8 Fonctions

Nous exprimons une fonction selon deux notations distinctes, soit celle de son type, qui exprime sa signature et explique comment l’appeler, et celle exprimant sa définition, son effet. À titre d’exemple, une fonction 𝑐𝑎𝑟𝑟é acceptant un entier en paramètre et ayant pour résultat un entier qui soit le carré dudit paramètre se présenterait comme suit :

𝑐𝑎𝑟𝑟é ∷ 𝑖𝑛𝑡 → 𝑖𝑛𝑡 𝑐𝑎𝑟𝑟é(𝑛: 𝑖𝑛𝑡) ≜ 𝑛 × 𝑛 ou, alternativement :

𝑐𝑎𝑟𝑟é ∷ 𝑖𝑛𝑡 → 𝑖𝑛𝑡 𝑐𝑎𝑟𝑟é(𝑛: 𝑖𝑛𝑡) ≜ 𝑛2

La première expression est la signature de la fonction, et indique que 𝑐𝑎𝑟𝑟é consomme un 𝑖𝑛𝑡 et a pour résultat un 𝑖𝑛𝑡. Nous admettons les fonctions génériques sur la base de types 𝑇, 𝑈, … avec l’écriture 𝑓〈𝑇, 𝑈〉 ∷ 𝑇 → 𝑈 ou simplement 𝑓 ∷ 𝑇 → 𝑈 si le contexte s’y prête. À titre d’exemple :

𝑐𝑎𝑟𝑟é〈𝑇〉 ∷ 𝑇 → 𝑇 𝑐𝑎𝑟𝑟é(𝑛: 𝑇) ≜ 𝑛 × 𝑛

De manière alternative, si cela n’obscurcit pas le propos, nous irons même jusqu’à simplifier l’écriture de la description de l’effet d’une fonction en omettant les types de ses paramètres :

𝑐𝑎𝑟𝑟é(𝑛) ≜ 𝑛 × 𝑛

Dans le cas où une fonction a des implémentations variant selon la forme de ses paramètres, nous offrons plus d’une définition pour son effet. Par exemple, pour exprimer un prédicat 𝑒𝑚𝑝𝑡𝑦 applicable à une liste, nous pourrions avoir :

𝑒𝑚𝑝𝑡𝑦 ∷ 𝐿𝑖𝑠𝑡〈𝑇〉 → 𝑏𝑜𝑜𝑙 𝑒𝑚𝑝𝑡𝑦([ℎ| … ]) ≜ 𝑓𝑎𝑙𝑠𝑒

𝑒𝑚𝑝𝑡𝑦([ ]) ≜ 𝑡𝑟𝑢𝑒

Nous acceptons un même nom pour plusieurs signatures de types distinctes. Par exemple, 𝑒𝑚𝑝𝑡𝑦 applicable à une 𝐿𝑖𝑠𝑡〈𝑇〉 peut coexister avec 𝑒𝑚𝑝𝑡𝑦 applicable à un 𝑆𝑒𝑡〈𝑇〉 sans que cela ne cause d’ambiguïté en pratique dans ContextAA.

79

Nous supposons, dans notre notation, le Currying, qui permet de traiter une fonction d’arité 𝑛: ℕ, 𝑛 > 0 comme une fonction acceptant un paramètre et retournant une fonction d’arité 𝑛 − 1. Pour cette raison, une fonction comme (notation C) :

int somme(int x, int y) { return x + y } s’exprimera, dans notre formalisme, sous la forme :

𝑠𝑜𝑚𝑚𝑒 ∷ 𝑖𝑛𝑡 → 𝑖𝑛𝑡 → 𝑖𝑛𝑡 𝑠𝑜𝑚𝑚𝑒(𝑥, 𝑦) ≜ 𝑥 + 𝑦 où la signature de types suppose visiblement le Currying.

Dans le cas de foncteurs ou d’expressions λ, qui permettent la fermeture (§4.2.9) sur des états capturés du contexte lors de l’appel, nous utilisons la notation suivante :

𝑚𝑒𝑚𝑒𝑉𝑎𝑙𝑒𝑢𝑟𝑄𝑢𝑒 ∶: 𝑖𝑛𝑡 → (𝑖𝑛𝑡 → 𝑏𝑜𝑜𝑙) → 𝑏𝑜𝑜𝑙 𝑚𝑒𝑚𝑒𝑉𝑎𝑙𝑒𝑢𝑟𝑄𝑢𝑒(𝑣𝑎𝑙: 𝑖𝑛𝑡)(𝑛: 𝑖𝑛𝑡) ≜ 𝑣𝑎𝑙 = 𝑛

Dans cette notation, 𝑣𝑎𝑙 est capturé à la construction de 𝑚𝑒𝑚𝑒𝑉𝑎𝑙𝑒𝑢𝑟𝑄𝑢𝑒, alors que 𝑛 est utilisé par la suite chaque fois que le 𝑚𝑒𝑚𝑒𝑉𝑎𝑙𝑒𝑢𝑟𝑄𝑢𝑒 est utilisé comme une fonction. Cela est similaire à ce qui est fait dans le langage C++, où 𝑚𝑒𝑚𝑒𝑉𝑎𝑙𝑒𝑢𝑟𝑄𝑢𝑒 serait un foncteur ou une λ (𝑣𝑎𝑙 serait capturé à la construction), alors que dans un langage fonctionnel, ce serait une étape intermédiaire d’une séquence de Currying.

Pour simplifier l’expression de certains algorithmes, nous utilisons : 𝑏𝑜𝑜𝑙𝑇𝑜𝐼𝑛𝑡 ∷ 𝑏𝑜𝑜𝑙 → (0,1)

𝑏𝑜𝑜𝑙𝑇𝑜𝐼𝑛𝑡(𝑡𝑟𝑢𝑒) ≜ 1 𝑏𝑜𝑜𝑙𝑇𝑜𝐼𝑛𝑡(𝑓𝑎𝑙𝑠𝑒) ≜ 0

Dans le cas d’une surcharge d’un opérateur 𝑜𝑝, nous exprimons sous la forme (𝑜𝑝) la signature de la fonction résultante :

(<) ∷ 𝑆𝑒𝑡 → 𝑆𝑒𝑡 → 𝑏𝑜𝑜𝑙 (<)(𝑠, 𝑠′) ≜ |𝑠| < |𝑠|

4.2.9 Fermetures

À titre utilitaire, nous utilisons parfois la capacité de construire une fermeture sur un paramètre, exprimée de manière générique ci-dessous :

𝑏𝑖𝑛𝑑 ∷ 𝐹𝑢𝑛𝑐 → 𝐴𝑟𝑔 → (𝐴𝑟𝑔′→ 𝑅𝑒𝑠𝑢𝑙𝑡) → 𝑅𝑒𝑠𝑢𝑙𝑡 𝑏𝑖𝑛𝑑(𝑓, 𝑎) ≜ (𝐹𝑢𝑛𝑐(𝑥: 𝐴𝑟𝑔′) ≜ 𝑓(𝑎, 𝑥))

Dans cette écriture, 𝐹𝑢𝑛𝑐 est une fonction au sens générique du terme, et 𝑅𝑒𝑠𝑢𝑙𝑡 est le type du résultat de son application sur un paramètre donné. Exprimé en mots, 𝑏𝑖𝑛𝑑(𝑓, 𝑥) retourne une opération 𝑔 telle que 𝑔(𝑦) ≡ 𝑓(𝑥, 𝑦). Sur le plan technique, la résolution du type de 𝑔(𝑦), donc de 𝑓(𝑥, 𝑦), est faite de manière statique à partir de la signature des types impliqués.

80

De même, pour alléger certaines écritures, nous utilisons aussi des outils typiques de la programmation fonctionnelle tels que :

𝑚𝑎𝑝 → 𝐹𝑢𝑛𝑐 → 𝐿𝑖𝑠𝑡〈𝑇〉 → 𝐿𝑖𝑠𝑡〈𝑑𝑒𝑐𝑙𝑡𝑦𝑝𝑒(𝐹𝑢𝑛𝑐(𝑇))〉 𝑚𝑎𝑝(𝑓, [ ]) ≜ 𝑓([ ])

𝑚𝑎𝑝(𝑓, [ℎ|𝑡]) ≜ [𝑓(ℎ)|𝑚𝑎𝑝(𝑓, 𝑡)]

La notation 𝑑𝑒𝑐𝑙𝑡𝑦𝑝𝑒(𝑒𝑥𝑝𝑟) prend le sens de type de l’expression 𝑒𝑥𝑝𝑟. Opérant sur la base des types seuls, elle n’évalue pas la valeur de l’expression 𝑒𝑥𝑝𝑟.