web-dev-qa-db-fra.com

Abuser de l’algèbre des types de données algébriques - pourquoi cela fonctionne-t-il?

L'expression "algébrique" pour les types de données algébriques semble très suggestive pour une personne ayant une formation en mathématiques. Laissez-moi essayer d'expliquer ce que je veux dire.

Ayant défini les types de base

  • Produit
  • Union +
  • Singleton X
  • Unité 1

et en utilisant le raccourci pour X•X et 2X pour X+X, etc., nous pouvons ensuite définir des expressions algébriques pour, par exemple. listes liées

data List a = Nil | Cons a (List a)L = 1 + X • L

et arbres binaires:

data Tree a = Nil | Branch a (Tree a) (Tree a)T = 1 + X • T²

Maintenant, mon premier instinct en tant que mathématicien est de devenir fou avec ces expressions et d’essayer de résoudre pour L et T. Je pouvais le faire par des substitutions répétées, mais il semble beaucoup plus facile d’abuser de la notation de manière horrible et de prétendre pouvoir la réorganiser à volonté. Par exemple, pour une liste chaînée:

L = 1 + X • L

(1 - X) • L = 1

L = 1 / (1 - X) = 1 + X + X² + X³ + ...

où j’ai utilisé l’expansion en série de 1 / (1 - X) d’une manière totalement injustifiée pour obtenir un résultat intéressant, à savoir qu’un type L est soit Nil, ou il contient 1 élément, ou il contient 2 éléments, ou 3, etc.

Cela devient plus intéressant si nous le faisons pour les arbres binaires:

T = 1 + X • T²

X • T² - T + 1 = 0

T = (1 - √(1 - 4 • X)) / (2 • X)

T = 1 + X + 2 • X² + 5 • X³ + 14 • X⁴ + ...

à nouveau, en utilisant l’extension de la série power (réalisée avec Wolfram Alpha ). Ceci exprime le fait non évident (pour moi) qu'il n'y a qu'un seul arbre binaire avec 1 élément, 2 arbres binaires avec deux éléments (le deuxième élément peut être sur la branche gauche ou droite), 5 arbres binaires avec trois éléments, etc. .

Donc ma question est - qu'est-ce que je fais ici? Ces opérations semblent injustifiées (quelle est exactement la racine carrée d'un type de données algébriques de toute façon?) Mais aboutissent à des résultats raisonnables. le quotient de deux types de données algébriques a-t-il une signification en informatique, ou s'agit-il simplement d'une ruse de notation?

Et, peut-être plus intéressant encore, est-il possible d'étendre ces idées? Existe-t-il une théorie de l'algèbre des types qui autorise, par exemple, des fonctions arbitraires sur les types, ou les types nécessitent-ils une représentation en série? Si vous pouvez définir une classe de fonctions, la composition des fonctions a-t-elle un sens?

275
Chris Taylor

Clause de non-responsabilité: Cela ne fonctionne pas vraiment très bien lorsque vous tenez compte de ⊥, je vais donc l'ignorer de manière flagrante pour des raisons de simplicité.

Quelques points initiaux:

  • Notez que "union" n'est probablement pas le meilleur terme pour A + B ici - c'est spécifiquement ne disjoint ​​union des deux types, car les deux côtés sont distincts même si leurs types sont les mêmes. Pour ce que cela vaut, le terme le plus courant est simplement "type de somme".

  • Les types singleton sont, en réalité, tous les types d'unités. Ils se comportent de manière identique lors de manipulations algébriques et, ce qui est plus important encore, la quantité d'informations présentes est toujours préservée.

  • Vous voulez probablement aussi un type zéro. Il n'y en a pas de standard, mais le nom le plus courant est Void. Il n'y a pas de valeurs dont le type est zéro, tout comme il existe une valeur dont le type est un.

Il manque encore une opération majeure ici, mais j'y reviendrai dans un instant.

Comme vous l'avez probablement remarqué, Haskell a tendance à emprunter des concepts de la théorie des catégories, et tout ce qui précède a une interprétation très simple en tant que telle:

  • Étant donné les objets A et B dans Hask , leur produit A × B est le type unique (jusqu'à l'isomorphisme) qui permet deux projections fst: A × B → A et snd: A × B → B, quel que soit le type C et les fonctions f: C → A, g: C → B vous pouvez définir le couplage f &&& g: C → A × B tel que fst ∘ (f &&& g) = f et de même pour g. La paramétrisation garantit automatiquement les propriétés universelles et mon choix de noms moins que subtils devrait vous en donner l'idée. L'opérateur (&&&) Est d'ailleurs défini dans Control.Arrow.

  • Le dual de ce qui précède est le coproduit A + B avec des injections inl: A → A + B et inr: B → A + B, où sont donnés n'importe quel type C et fonctions f: A → C, g: B → C, vous pouvez définir le copairage f ||| g: A + B → C tels que les équivalences évidentes sont vérifiées. Encore une fois, la paramétrie garantit automatiquement la plupart des parties difficiles. Dans ce cas, les injections standard sont simplement Left et Right et le copairage est la fonction either.

De nombreuses propriétés des types de produit et de somme peuvent être déduites de ce qui précède. Notez que tout type de singleton est un objet terminal de Hask et que tout type vide est un objet initial.

Pour revenir à l'opération manquante susmentionnée, dans un catégorie fermée cartésienne vous avez objets exponentiels qui correspondent aux flèches de la catégorie. Nos flèches sont des fonctions, nos objets sont des types de type *, Et le type A -> B Se comporte bien comme BUNE dans le contexte de la manipulation algébrique de types. Si la raison de cette situation n’est pas évidente, considérez le type Bool -> A. Avec seulement deux entrées possibles, une fonction de ce type est isomorphe à deux valeurs de type A, c'est-à-dire (A, A). Pour Maybe Bool -> A, Nous avons trois entrées possibles, et ainsi de suite. De plus, notez que si nous reformulons la définition de copairing ci-dessus pour utiliser la notation algébrique, nous obtenons l’identité CUNE × CB = CA + B.

En ce qui concerne pourquoi tout cela a du sens - et en particulier pourquoi votre utilisation de l'extension de la série de puissance est justifiée - notez que la majeure partie de ce qui précède fait référence aux "habitants" d'un type (c'est-à-dire distincts valeurs ayant ce type) afin de démontrer le comportement algébrique. Pour rendre cette perspective explicite:

  • Le type de produit (A, B) Représente une valeur de A et B, pris indépendamment l'un de l'autre. Donc, pour toute valeur fixe a :: A, Il existe une valeur de type (A, B) Pour chaque habitant de B. Il s’agit bien entendu du produit cartésien, et le nombre d’habitants du type de produit correspond au produit du nombre d’habitants du facteur.

  • Le type sum Either A B Représente une valeur de A ou B, en distinguant les branches gauche et droite. Comme mentionné précédemment, il s'agit d'une union disjointe et le nombre d'habitants du type somme est la somme du nombre d'habitants des sommets.

  • Le type exponentiel B -> A Représente un mappage des valeurs de type B aux valeurs de type A. Pour tout argument fixe b :: B, Toute valeur de A peut lui être affectée; une valeur de type B -> A sélectionne un tel mappage pour chaque entrée, ce qui équivaut à un produit d'autant de copies de A que de B a des habitants, d'où l'exponentiation.

Bien qu’il soit tentant au départ de traiter les types comme des ensembles, cela ne fonctionne pas très bien dans ce contexte: nous avons une union disjointe plutôt que l’union standard d’ensembles, il n’existe aucune interprétation évidente de l’intersection ou de nombreuses autres opérations d’ensembles. ne s’inquiète généralement pas de l’appartenance définie (en laissant cela au vérificateur de type).

D'autre part, les constructions ci-dessus passent beaucoup de temps à parler de compter habitants, et énumérer les valeurs possibles d'un type sont un concept utile ici. Cela nous amène rapidement à combinatoire énumérative , et si vous consultez l’article lié à Wikipedia, vous constaterez qu’une des premières choses qu’il fait est de définir les "paires" et les "unions" dans le même sens que Les types product et sum au moyen de fonctions génératrices , puis effectue la même chose pour les "séquences" identiques aux listes de Haskell en utilisant exactement la même technique que vous avez utilisée.


Edit: Oh, et voici un bonus rapide qui, à mon avis, illustre ce point de manière frappante. Vous avez mentionné dans un commentaire que pour un type d'arborescence T = 1 + T^2, Vous pouvez dériver l'identité T^6 = 1, Ce qui est clairement faux. Cependant, T^7 = Tne tient, et une bijection entre arbres et sept-uplets d'arbres peut être construite directement, cf. "Sept arbres en un" d'Andreas Blass .

Edit × 2: À propos de la construction du "dérivé d'un type" mentionnée dans d'autres réponses, vous pourriez également apprécier cet article de même auteur qui s'appuie sur l'idée plus avant, y compris les notions de division et autres choses intéressantes.

135
C. A. McCann

Les arbres binaires sont définis par l'équation T=1+XT^2 Dans le semiring de types. Par construction, T=(1-sqrt(1-4X))/(2X) est défini par la même équation dans le semiring de nombres complexes. Donc, étant donné que nous résolvons la même équation dans la même classe de structure algébrique, il n’est pas surprenant que nous voyions des similitudes.

Le problème est que lorsque nous raisonnons sur les polynômes lors de la semiation de nombres complexes, nous utilisons généralement le fait que les nombres complexes forment un anneau ou même un corps, ce qui nous oblige à utiliser des opérations telles que la soustraction qui ne s'applique pas aux semirings. Mais nous pouvons souvent éliminer les soustractions de nos arguments si nous avons une règle qui nous permet d'annuler des deux côtés d'une équation. C'est le genre de chose prouvé par Fiore et Leinster montrant que beaucoup d'arguments concernant les anneaux peuvent être transférés à des semirings.

Cela signifie que beaucoup de vos connaissances mathématiques sur les anneaux peuvent être transférées de manière fiable aux types. En conséquence, certains arguments impliquant des nombres complexes ou des séries de puissances (dans l’anneau des séries formelles de puissances) peuvent être transmis de manière totalement rigoureuse aux types.

Cependant, il y a plus dans l'histoire que cela. C'est une chose de prouver que deux types sont égaux (par exemple) en montrant que deux séries de puissance sont égales. Mais vous pouvez également déduire des informations sur les types en inspectant les termes de la série power. Je ne suis pas sûr de ce que la déclaration officielle devrait être. (Je recommande Brent Yorgey papier sur espèce combinatoire pour certains travaux étroitement liés, mais les espèces ne sont pas les mêmes que les types.)

Ce que je trouve vraiment hallucinant est que ce que vous avez découvert puisse être étendu au calcul. Les théorèmes sur le calcul peuvent être transférés au semiring de types. En fait, même les arguments sur les différences finies peuvent être transférés et vous constatez que les théorèmes classiques issus de l'analyse numérique ont des interprétations en théorie des types.

S'amuser!

43
sigfpe

Il semble que tout ce que vous faites est d'élargir la relation de récurrence.

L = 1 + X • L
L = 1 + X • (1 + X • (1 + X • (1 + X • ...)))
  = 1 + X + X^2 + X^3 + X^4 ...

T = 1 + X • T^2
L = 1 + X • (1 + X • (1 + X • (1 + X • ...^2)^2)^2)^2
  = 1 + X + 2 • X^2 + 5 • X^3 + 14 • X^4 + ...

Et comme les règles des opérations sur les types fonctionnent comme celles des opérations arithmétiques, vous pouvez utiliser des moyens algébriques pour vous aider à comprendre comment développer la relation de récurrence (car elle n’est pas évidente).

20
newacct

Je n'ai pas de réponse complète, mais ces manipulations ont tendance à "fonctionner". Un article pertinent pourrait être Objets de catégories en tant que nombres complexes de Fiore et Leinster - Je suis tombé sur celui-ci en lisant le blog de sigfpe sur un sujet connexe ; le reste de ce blog est une mine d'or pour des idées similaires et mérite le détour!

Vous pouvez également différencier les types de données, ce qui vous permettra d'obtenir le zip approprié pour le type de données!

18
yatima2975

L'algèbre des processus de communication (ACP) traite des types d'expressions similaires pour les processus. Il offre l'addition et la multiplication en tant qu'opérateurs de choix et de séquence, avec des éléments neutres associés. Sur cette base, il existe des opérateurs pour d'autres constructions, telles que le parallélisme et la perturbation. Voir http://en.wikipedia.org/wiki/Algebra_of_Communicating_Processes . Il existe également un article en ligne intitulé "Une brève histoire de l’algèbre des processus".

Je travaille sur l'extension des langages de programmation avec ACP. En avril dernier, j'ai présenté un document de recherche à Scala Days 2012, disponible à l'adresse http://code.google.com/p/subscript/

Lors de la conférence, j'ai présenté un débogueur exécutant une spécification récursive parallèle d'un sac:

Sac = A; (Sac & a)

où A et a représentent les actions d'entrée et de sortie; le point-virgule et l'esperluette signifient séquence et parallélisme. Voir la vidéo sur SkillsMatter, accessible depuis le lien précédent.

Une spécification de sac plus comparable à

L = 1 + X • L

serait

B = 1 + X & B

ACP définit le parallélisme en termes de choix et de séquence en utilisant des axiomes; voir l'article de Wikipedia. Je me demande quelle serait l'analogie du sac

L = 1/(1-X)

La programmation de style ACP est pratique pour les analyseurs de texte et les contrôleurs d'interface graphique. Des spécifications telles que

searchCommand = cliqué (searchButton) + clé (entrée)

cancelCommand = clicked (cancelButton) + key (Escape)

peut être écrit de manière plus concise en rendant les deux raffinements "cliqué" et "clé" implicites (comme quoi Scala permet avec des fonctions). Par conséquent, nous pouvons écrire:

searchCommand = searchButton + Entrée

cancelCommand = cancelButton + Escape

Le côté droit contient maintenant des opérandes qui sont des données plutôt que des processus. A ce niveau, il n'est pas nécessaire de savoir quelles améliorations implicites transformeront ces opérandes en processus; ils ne seraient pas nécessairement raffinés en actions d'entrée; les actions de sortie s'appliqueraient également, par exemple: dans la spécification d'un robot de test.

Les processus obtiennent ces données sous forme de compagnons; ainsi je forge le terme "algèbre d'objets".

10
André van Delft

Série Calcul et Maclaurin avec types

Voici un autre ajout mineur - un aperçu combinatoire des raisons pour lesquelles les coefficients d'un développement de série devraient "fonctionner", en particulier en se concentrant sur les séries pouvant être dérivées à l'aide de l'approche Taylor-Maclaurin à partir du calcul. NB: l’exemple d’extension de série manipulé que vous donnez est une série de Maclaurin.

Comme d'autres réponses et commentaires traitent du comportement des expressions de type algébrique (sommes, produits et exposants), cette réponse éludera ce détail et se concentrera sur le type 'calcul'.

Vous remarquerez peut-être des guillemets qui soulèvent des questions lourdes dans cette réponse. Il y a deux raisons:

  • nous sommes en train de donner des interprétations d'un domaine à des entités d'un autre et il semble approprié de délimiter de la sorte de telles notions étrangères.
  • certaines notions pourront être formalisées plus rigoureusement, mais la forme et les idées semblent plus importantes (et prennent moins de place pour écrire) que les détails.

Définition de la série Maclaurin

Le série Maclaurin d'une fonction f : ℝ → ℝ Est défini comme

f(0) + f'(0)X + (1/2)f''(0)X² + ... + (1/n!)f⁽ⁿ⁾(0)Xⁿ + ...

f⁽ⁿ⁾ signifie la dérivée nth de f.

Pour pouvoir interpréter la série de Maclaurin interprétée avec les types, nous devons comprendre comment interpréter trois choses dans un contexte de type:

  • un dérivé (éventuellement multiple)
  • appliquer une fonction à 0
  • termes comme (1/n!)

et il s'avère que ces concepts d'analyse ont des contreparties appropriées dans le monde des types.

Qu'est-ce que je veux dire par "contrepartie appropriée"? Cela devrait avoir la saveur d’un isomorphisme - si nous pouvons préserver la vérité dans les deux sens, les faits pouvant être dérivés dans un contexte peuvent être transférés dans un autre.

Calcul avec types

Alors, que signifie l'expression d'une expression de type? Il s'avère que, pour une classe d'expressions de type et de foncteurs large et bien conçue ('différentiable'), il existe une opération naturelle qui se comporte de manière similaire pour être une interprétation appropriée!

Pour gâcher la ligne de frappe, l'opération analogue à la différenciation consiste à créer des "contextes à un trou". This est un excellent endroit pour développer davantage ce point particulier, mais le concept de base d’un contexte à un trou (da/dx) Est qu’il représente le résultat de l’extraction d’un seul sous-élément d’un objet. type particulier (x) d'un terme (de type a) conservant toutes les autres informations, y compris celles nécessaires pour déterminer l'emplacement d'origine du sous-élément. Par exemple, une manière de représenter un contexte à un trou pour une liste consiste à utiliser deux listes: une pour les éléments précédant l'extraction et une autre pour les éléments suivants.

La motivation pour identifier cette opération avec différenciation provient des observations suivantes. Nous écrivons da/dx Pour signifier le type de contextes à un trou pour le type a avec le trou de type x.

d1/dx = 0
dx/dx = 1
d(a + b)/dx = da/dx + db/dx
d(a × b)/dx = a × db/dx + b × da/dx
d(g(f(x))/dx = d(g(y))/dy[f(x)/a] × df(x)/dx

Ici, 1 Et 0 Représentent des types ayant exactement un et exactement zéro habitants, respectivement, et + Et × Représentent des types de somme et de produit comme d'habitude. f et g sont utilisés pour représenter des fonctions de type, ou des formateurs d'expression de type, et [f(x)/a] signifie l'opération consistant à substituer f(x) pour chaque a dans l'expression précédente.

Ceci peut être écrit dans un style sans point, en écrivant f' Pour signifier la fonction dérivée de type function f, ainsi:

(x ↦ 1)' = x ↦ 0
(x ↦ x)' = x ↦ 1
(f + g)' = f' + g'
(f × g)' = f × g' + g × f'
(g ∘ f)' = (g' ∘ f) × f'

ce qui peut être préférable.

NB les égalités peuvent être rendues rigoureuses et exactes si nous définissons des dérivés en utilisant des classes de types et de foncteurs d’isomorphisme.

Nous remarquons en particulier que les règles de calcul relatives aux opérations algébriques d’addition, de multiplication et de composition (souvent appelées règles de somme, de produit et de chaîne) sont reflétées exactement par l’opération consistant à "faire un trou". En outre, les cas de base de 'faire un trou' dans une expression constante ou le terme x se comportent également comme une différenciation. Ainsi, par induction, nous obtenons un comportement semblable à la différenciation pour toutes les expressions de type algébrique.

Maintenant, nous pouvons interpréter la différenciation. Que signifie le nth 'dérivé' d'une expression de type, dⁿe/dxⁿ? C'est un type représentant les contextes n- place: termes qui, lorsqu'ils sont 'remplis' par n termes de type x donnent un e. Une autre observation clé liée à "(1/n!)" Vient plus tard.

La partie invariante d'un type foncteur: appliquer une fonction à 0

Nous avons déjà une interprétation pour 0 Dans le monde des types: un type vide sans membres. Qu'est-ce que cela signifie, d'un point de vue combinatoire, de lui appliquer une fonction type? Plus concrètement, en supposant que f soit une fonction type, à quoi ressemble f(0)? Eh bien, nous n’avons certainement accès à aucun élément de type 0, De sorte que toutes les constructions de f(x) qui nécessitent un x ne sont pas disponibles. Ce qui reste sont les termes qui sont accessibles en leur absence, que nous pouvons appeler la partie "invariante" ou "constante" du type.

Pour un exemple explicite, prenons le foncteur Maybe qui peut être représenté algébriquement par x ↦ 1 + x. Lorsque nous appliquons cela à 0, Nous obtenons 1 + 0 - c'est comme 1: La seule valeur possible est la valeur None. De même, pour une liste, nous n'obtenons que le terme correspondant à la liste vide.

Lorsque nous le rapportons et interprétons le type f(0) comme un nombre, on peut le considérer comme le compte de combien de termes de type f(x) ( pour tout x) peut être obtenu sans accès à un x: c'est-à-dire le nombre de termes de type "vide-like".

Assemblage: interprétation complète d'une série de Maclaurin

J'ai bien peur de ne pas pouvoir concevoir une interprétation directe appropriée de (1/n!) Comme type.

Si nous considérons cependant le type f⁽ⁿ⁾(0) à la lumière de ce qui précède, nous voyons qu'il peut être interprété comme le type de contextes n-place pour un terme de type f(x) qui ne contient pas déjà unx - c’est-à-dire que lorsque nous les "intégrons" n fois, le terme résultant a exactementnxs, ni plus, ni moins. Ensuite, l’interprétation du type f⁽ⁿ⁾(0) sous forme de nombre (comme dans les coefficients de la série de Maclaurin de f) est simplement un compte du nombre de ces n vides vides contextes il y a. Nous y sommes presque!

Mais où finit (1/n!)? L'examen du processus de type 'différenciation' nous montre que, lorsqu'il est appliqué plusieurs fois, il préserve l'ordre dans lequel les sous-termes sont extraits. Par exemple, considérons le terme (x₀, x₁) De type x × x Et l'opération consistant à "faire un trou" à deux reprises. Nous obtenons les deux séquences

(x₀, x₁)  ↝  (_₀, x₁)  ↝  (_₀, _₁)
(x₀, x₁)  ↝  (x₀, _₀)  ↝  (_₁, _₀)
(where _ represents a 'hole')

même si les deux viennent du même terme, car il y a 2! = 2 des manières de prendre deux éléments sur deux, en préservant l'ordre. En général, il y a n! façons de prendre n éléments de n. Donc, pour obtenir le nombre de configurations d’un type de foncteur qui ont des éléments n, nous devons compter le type f⁽ⁿ⁾(0) et le diviser par n!, = exactement comme dans les coefficients de la série de Maclaurin.

Donc, diviser par n! S'avère être interprétable simplement comme tel.

Réflexions finales: définitions et analyse "récursives"

Tout d'abord, quelques observations:

  • si une fonction f: → ℝ a une dérivée, cette dérivée est unique
  • de même, si une fonction f: ℝ → est analytique, elle a exactement une série polynomiale correspondante

Puisque nous avons la règle de chaîne, nous pouvons utiliser différenciation implicite , si nous formalisons les dérivés de type en tant que classes d'isomorphisme. Mais la différenciation implicite ne nécessite aucune manœuvre extraterrestre telle que la soustraction ou la division! Nous pouvons donc l'utiliser pour analyser les définitions de type récursif. Pour prendre votre exemple de liste, nous avons

L(X) ≅ 1 + X × L(X)
L'(X) = X × L'(X) + L(X)

et alors nous pouvons évaluer

L'(0) = L(0) = 1

pour obtenir le coefficient de dans la série Maclaurin.

Mais puisque nous sommes convaincus que ces expressions sont en effet strictement 'différentiables', ne serait-ce qu'implicitement, et que nous avons la correspondance avec les fonctions ℝ → ℝ, où les dérivés sont certainement uniques, nous pouvons être assurés que même si nous obtenons les valeurs avec opérations illégales, le résultat est valide.

De même, pour utiliser la deuxième observation, due à la correspondance (est-ce un homomorphisme?) Avec les fonctions ℝ →, nous savons que, à condition que nous soyons convaincus qu'une fonction a un Maclaurin série, si nous pouvons trouver aucune série du tout, les principes décrits ci-dessus peuvent être appliqués pour le rendre rigoureux.

En ce qui concerne votre question sur la composition des fonctions, je suppose que la règle de la chaîne fournit une réponse partielle.

Je ne suis pas certain du nombre de TDA de style Haskell auxquels cela s'applique, mais je suppose que c'est beaucoup, voire tous. J'ai découvert une preuve vraiment merveilleuse de ce fait, mais cette marge est trop petite pour la contenir ...

Maintenant, ce n’est certainement qu’un moyen de déterminer ce qui se passe ici et il existe probablement de nombreux autres moyens.

Résumé: TL; DR

  • le type 'différenciation' correspond à ' faire un tro '.
  • appliquer un foncteur à 0 nous donne les termes "vides" pour ce foncteur.
  • Maclaurin Les séries de puissances correspondent donc (quelque peu) rigoureusement à l'énumération du nombre de membres d'un type de foncteur avec un certain nombre d'éléments.
  • différenciation implicite rend cela plus étanche.
  • le caractère unique des dérivés et le caractère unique des séries de puissance signifient que nous pouvons tromper les détails et que cela fonctionne.
6
Oly

Théorie des types dépendants et fonctions du type 'arbitraire'

Ma première réponse à cette question portait sur les concepts, mais sur les détails, et portait sur la sous-question "Que se passe-t-il? cette réponse sera la même mais centrée sur la sous-question "Peut-on obtenir des fonctions de type arbitraire?".

Une extension aux opérations algébriques de somme et produit sont les "grands opérateurs", qui représentent la somme et le produit d'une séquence (ou plus généralement, la somme et le produit d'une fonction sur un domaine) habituellement écrit Σ et Π respectivement. Voir Notation Sigma .

Donc la somme

a₀ + a₁X + a₂X² + ...

pourrait être écrit

Σ[i ∈ ℕ]aᵢXⁱ

a est une séquence de nombres réels, par exemple. Le produit serait représenté de la même manière avec Π Au lieu de Σ.

Quand vous regardez de loin, ce genre d'expression ressemble beaucoup à une fonction 'arbitraire' dans X; nous sommes bien sûr limités aux séries exprimables et à leurs fonctions analytiques associées. Est-ce un candidat à une représentation dans une théorie des types? Absolument!

La classe des théories de type qui ont des représentations immédiates de ces expressions est la classe des théories de type "dépendantes": théories avec types dépendants. Naturellement, nous avons des termes qui dépendent de termes et dans des langages comme Haskell avec des fonctions de type et une quantification de type, des termes et des types dépendant de types. Dans un paramètre dépendant, nous avons également des types en fonction des termes. Haskell n'est pas un langage typé de manière dépendante, bien que de nombreuses fonctionnalités de types dépendants puissent être simulées en torturant un peu la langue .

Curry-Howard et types dépendants

L'isomorphisme de Curry-Howard a commencé sa vie en constatant que les termes et les règles de jugement de type du lambda calcul simplement typé correspondaient exactement à la déduction naturelle (telle que formulée par Gentzen) appliquée à la logique propositionnelle intuitionniste, les types prenant la place des propositions , et les termes se substituant aux preuves, bien que les deux soient inventés/découverts indépendamment. Depuis lors, il a été une source d'inspiration énorme pour les théoriciens des types. L'une des choses les plus évidentes à considérer est de savoir si et comment cette correspondance pour la logique propositionnelle peut être étendue aux logiques de prédicats ou d'ordre supérieur. Les théories de type dépendantes sont initialement issues de cette avenue d'exploration.

Pour une introduction à l'isomorphisme de Curry-Howard pour le calcul lambda simplement typé, voir ici . Par exemple, si nous voulons prouver A ∧ B, Nous devons prouver A et prouver B; une preuve combinée est simplement une paire de preuves: une pour chaque conjonction.

En déduction naturelle:

Γ ⊢ A    Γ ⊢ B
Γ ⊢ A ∧ B

et dans le calcul lambda simplement typé:

Γ ⊢ a : A    Γ ⊢ b : B
Γ ⊢ (a, b) : A × B

Des correspondances similaires existent pour Et les types de somme, Et les types de fonction et les diverses règles d'élimination.

Une proposition non démontable (fausse sur le plan intuitionniste) correspond à un type inhabité.

Avec l'analogie des types en tant que propositions logiques à l'esprit, nous pouvons commencer à réfléchir à la manière de modéliser les prédicats dans le monde des types. Cela a été formalisé de nombreuses manières (voir cette introduction à la théorie de types intuitionniste de Martin-Löf pour une norme largement utilisée), mais l'approche abstraite observe généralement qu'un prédicat est comme une proposition avec free variables de terme, ou, alternativement, une fonction prenant des termes à des propositions. Si nous permettons aux expressions de type de contenir des termes, un traitement dans le style du lambda calcul se présente immédiatement comme une possibilité!

En considérant seulement les preuves constructives, qu'est-ce qui constitue une preuve de ∀x ∈ X.P(x)? Nous pouvons le considérer comme une fonction de preuve, prenant les termes (x) pour vérifier les propositions correspondantes (P(x)). Ainsi, les membres (preuves) du type (proposition) ∀x : X.P(x) sont des "fonctions dépendantes", qui pour chaque x dans X donnent un terme de type P(x).

Qu'en est-il de ∃x ∈ X.P(x)? Nous avons besoin de tout membre de X, x, accompagné d'une preuve de P(x). Ainsi, les membres (preuves) du type (proposition) ∃x : X.P(x) sont des "paires dépendantes": un terme distingué x dans X, ainsi qu'un terme de type P(x).

Notation: je vais utiliser

∀x ∈ X...

pour les déclarations réelles concernant les membres de la classe X, et

∀x : X...

pour les expressions de type correspondant à la quantification universelle sur le type X. De même pour .

Considérations combinatoires: produits et sommes

En plus de la correspondance de Curry-Howard des types avec les propositions, nous avons la correspondance combinatoire des types algébriques avec des nombres et des fonctions, qui est le point principal de cette question. Heureusement, cela peut être étendu aux types dépendants décrits ci-dessus!

Je vais utiliser la notation de module

|A|

pour représenter la 'taille' d'un type A, pour rendre explicite la correspondance décrite dans la question, entre types et nombres. Notez que ceci est un concept en dehors de la théorie; Je ne prétends pas qu'il doit y avoir un tel opérateur dans la langue.

Comptons les membres possibles (entièrement réduits, canoniques) de type

∀x : X.P(x)

qui est le type des fonctions dépendantes prenant les termes x de type X en termes de type P(x). Chacune de ces fonctions doit avoir une sortie pour chaque terme de X, et cette sortie doit être d'un type particulier. Pour chaque x dans X, cela donne donc |P(x)| 'choix' de sortie.

Le punchline est

|∀x : X.P(x)| = Π[x : X]|P(x)|

ce qui bien sûr n'a pas beaucoup de sens si X est IO (), mais est applicable aux types algébriques.

De même, un terme de type

∃x : X.P(x)

est le type de paires (x, p) avec p : P(x), donc, étant donné tout x dans X, nous pouvons construire une paire appropriée avec tout membre de P(x), donnant |P(x)| 'choix'.

Par conséquent,

|∃x : X.P(x)| = Σ[x : X]|P(x)|

avec les mêmes mises en garde.

Cela justifie la notation commune des types dépendants dans les théories utilisant les symboles Π Et Σ, Et de nombreuses théories brouillent la distinction entre "pour tous" et "produit" et entre "il y a" et "somme", en raison des correspondances susmentionnées.

Nous nous rapprochons!

Vecteurs: représentant des n-uplets dépendants

Pouvons-nous maintenant encoder des expressions numériques comme

Σ[n ∈ ℕ]Xⁿ

comme expressions de type?

Pas assez. Bien que nous puissions examiner de manière informelle la signification d'expressions telles que Xⁿ Dans Haskell, où X est un type et n un nombre naturel, c'est un abus de notation; c'est une expression de type contenant un nombre: distinctement pas une expression valide.

D'autre part, avec les types dépendants dans l'image, les types contenant des nombres sont précisément le point; en fait, les nuplets ou "vecteurs" dépendants sont un exemple très couramment cité de la façon dont les types dépendants peuvent fournir sécurité pragmatique au niveau des types pour des opérations telles que l'accès par liste . Un vecteur est juste une liste avec des informations de niveau type concernant sa longueur: c'est précisément ce que nous recherchons pour des expressions de type comme Xⁿ.

Pour la durée de cette réponse, laissez

Vec X n

être le type de longueur - n vecteurs de X - valeurs de type.

Techniquement, n est plutôt qu'un nombre naturel actuel, une représentation dans le système d'un nombre naturel. Nous pouvons représenter les nombres naturels (Nat) dans le style Peano comme zéro (0) Ou le successeur (S) d'un autre nombre naturel, et pour n ∈ ℕ J'écris ˻n˼ Pour signifier le terme dans Nat qui représente n. Par exemple, ˻3˼ Est S (S (S 0)).

Ensuite nous avons

|Vec X ˻n˼| = |X|ⁿ

pour tout n ∈ ℕ.

Types Nat: promotion des termes en types

Maintenant, nous pouvons encoder des expressions comme

Σ[n ∈ ℕ]Xⁿ

en tant que types. Cette expression particulière donnerait lieu à un type qui est bien sûr isomorphe au type de listes de X, comme indiqué dans la question. (Non seulement cela, mais du point de vue des catégories théoriques, la fonction type - qui est un foncteur - prenant X au type ci-dessus est naturellement isomorphique au foncteur List.)

Une dernière pièce du puzzle pour les fonctions 'arbitraires' est comment encoder, pour

f : ℕ → ℕ

des expressions comme

Σ[n ∈ ℕ]f(n)Xⁿ

afin que nous puissions appliquer des coefficients arbitraires à une série de puissance.

Nous comprenons déjà la correspondance des types algébriques avec les nombres, ce qui nous permet de mapper des types aux nombres et des fonctions de type aux fonctions numériques. Nous pouvons aussi aller dans l'autre sens! - en prenant un nombre naturel, il existe évidemment un type algébrique définissable avec autant de membres à terme, que nous ayons ou non des types dépendants. Nous pouvons facilement le prouver dehors de la théorie des types par induction. Ce dont nous avons besoin, c'est d'un moyen de mapper des nombres naturels en types, inside le système.

Une réalisation agréable est que, une fois que nous avons des types dépendants, la preuve par induction et la construction par récurrence deviennent intimement similaires - en fait, elles sont la même chose dans de nombreuses théories. Puisque nous pouvons prouver par induction qu'il existe des types qui répondent à nos besoins, ne devrions-nous pas être en mesure de les construire?

Il existe plusieurs façons de représenter des types au niveau du terme. Je vais utiliser ici une notation Haskellish imaginaire avec * Pour l'univers des types, lui-même généralement considéré comme un type dans un environnement dépendant.1

De même, il existe au moins autant de façons de noter ' - élimination' qu'il existe de théories de types dépendantes. Je vais utiliser une notation Haskellish avec correspondance de motif.

Nous avons besoin d'un mappage, α De Nat à *, Avec la propriété

∀n ∈ ℕ.|α ˻n˼| = n.

La pseudodéfinition suivante suffit.

data Zero -- empty type
data Successor a = Z | Suc a -- Successor ≅ Maybe

α : Nat -> *
α 0 = Zero
α (S n) = Successor (α n)

Nous voyons donc que l'action de α Reflète le comportement du successeur S, ce qui en fait une sorte d'homomorphisme. Successor est une fonction de type qui "ajoute un" au nombre de membres d'un type; c'est-à-dire |Successor a| = 1 + |a| pour tout a ayant une taille définie.

Par exemple α ˻4˼ (Qui est α (S (S (S (S 0))))), est

Successor (Successor (Successor (Successor Zero)))

et les termes de ce type sont

Z
Suc Z
Suc (Suc Z)
Suc (Suc (Suc Z))

nous donnant exactement quatre éléments: |α ˻4˼| = 4.

De même, pour tout n ∈ ℕ, Nous avons

|α ˻n˼| = n

comme demandé.

  1. Beaucoup de théories exigent que les membres de * Ne soient que des représentants de types, et une opération est fournie en tant que mappage explicite des termes de type * Sur leurs types associés. D'autres théories permettent aux types littéraux eux-mêmes d'être des entités de niveau terme.

Des fonctions "arbitraires"?

Nous avons maintenant l'appareil pour exprimer une série de puissance complètement générale en tant que type!

Les séries

Σ[n ∈ ℕ]f(n)Xⁿ

devient le type

∃n : Nat.α (˻f˼ n) × (Vec X n)

˻f˼ : Nat → Nat est une représentation appropriée dans le langage de la fonction f. Nous pouvons voir ceci comme suit.

|∃n : Nat.α (˻f˼ n) × (Vec X n)|
    = Σ[n : Nat]|α (˻f˼ n) × (Vec X n)|          (property of ∃ types)
    = Σ[n ∈ ℕ]|α (˻f˼ ˻n˼) × (Vec X ˻n˼)|        (switching Nat for ℕ)
    = Σ[n ∈ ℕ]|α ˻f(n)˼ × (Vec X ˻n˼)|           (applying ˻f˼ to ˻n˼)
    = Σ[n ∈ ℕ]|α ˻f(n)˼||Vec X ˻n˼|              (splitting product)
    = Σ[n ∈ ℕ]f(n)|X|ⁿ                           (properties of α and Vec)

À quel point est-ce "arbitraire"? Nous sommes limités non seulement aux coefficients entiers par cette méthode, mais aux nombres naturels. En dehors de cela, f peut être n'importe quoi, étant donné qu'un langage Turing Complete avec des types dépendants, nous pouvons représenter toute fonction analytique avec des coefficients de nombre naturel.

Je n'ai pas étudié l'interaction de ceci avec, par exemple, le cas fourni dans la question de List X ≅ 1/(1 - X) ou le sens possible de ce que de tels 'types' négatifs et non entiers pourraient avoir dans ce contexte.

Espérons que cette réponse permettra d’explorer à quel point nous pouvons aller avec des fonctions de type arbitraire.

5
Oly