Un combinateur Y est un concept informatique du côté «fonctionnel» des choses. La plupart des programmeurs ne connaissent pas grand-chose aux combinateurs, s'ils en ont même entendu parler.
- Qu'est-ce qu'un combinateur Y?
- Comment fonctionnent les combinateurs?
- À quoi servent-ils?
- Sont-ils utiles dans les langages procéduraux?
functional-programming
computer-science
theory
definition
combinators
Chris Ammerman
la source
la source
Réponses:
Si vous êtes prêt pour une longue lecture, Mike Vanier a une excellente explication . Pour faire court, il vous permet d'implémenter la récursivité dans un langage qui ne le supporte pas nécessairement nativement.
la source
Un combinateur Y est une "fonctionnelle" (une fonction qui opère sur d'autres fonctions) qui permet la récursivité, lorsque vous ne pouvez pas vous référer à la fonction de l'intérieur. Dans la théorie de l'informatique, il généralise la récursivité , en faisant abstraction de sa mise en œuvre, et en la séparant ainsi du travail réel de la fonction en question. L'avantage de ne pas avoir besoin d'un nom au moment de la compilation pour la fonction récursive est en quelque sorte un bonus. =)
Ceci est applicable dans les langues qui prennent en charge les fonctions lambda . La nature basée sur l' expression des lambdas signifie généralement qu'ils ne peuvent pas se référer à eux-mêmes par leur nom. Et contourner cela en déclarant la variable, en s'y référant, puis en lui affectant le lambda, pour compléter la boucle d'auto-référence, est fragile. La variable lambda peut être copiée et la variable d'origine réaffectée, ce qui rompt l'auto-référence.
Les combinateurs Y sont lourds à implémenter et souvent à utiliser dans les langages de type statique (ce qui est souvent le cas des langages procéduraux ), car généralement les restrictions de frappe nécessitent que le nombre d'arguments pour la fonction en question soit connu au moment de la compilation. Cela signifie qu'un combinateur y doit être écrit pour tout nombre d'arguments que l'on doit utiliser.
Voici un exemple de la façon dont l'utilisation et le fonctionnement d'un Y-Combinator, en C #.
L'utilisation d'un combinateur Y implique une manière "inhabituelle" de construire une fonction récursive. Vous devez d'abord écrire votre fonction comme un morceau de code qui appelle une fonction préexistante, plutôt que lui-même:
Ensuite, vous transformez cela en une fonction qui prend une fonction à appeler et renvoie une fonction qui le fait. C'est ce qu'on appelle une fonction, car elle prend une fonction et effectue une opération avec elle qui se traduit par une autre fonction.
Vous avez maintenant une fonction qui prend une fonction et renvoie une autre fonction qui ressemble à une factorielle, mais au lieu de s'appeler elle-même, elle appelle l'argument passé dans la fonction externe. Comment faites-vous cela factoriel? Passez la fonction intérieure à elle-même. Le Y-Combinator fait cela, en étant une fonction avec un nom permanent, qui peut introduire la récursivité.
Plutôt que l'appel factoriel lui-même, ce qui se passe est que le factoriel appelle le générateur factoriel (renvoyé par l'appel récursif à Y-Combinator). Et selon la valeur actuelle de t, la fonction renvoyée par le générateur appellera à nouveau le générateur, avec t - 1, ou retournera simplement 1, mettant fin à la récursivité.
C'est compliqué et cryptique, mais tout secoue au moment de l'exécution, et la clé de son fonctionnement est «l'exécution différée» et la rupture de la récursivité pour s'étendre sur deux fonctions. Le F interne est passé en argument , à appeler lors de l'itération suivante, uniquement si nécessaire .
la source
fix :: (a -> a) -> a
et lea
peut à son tour être une fonction d'autant d'arguments que vous le souhaitez. Cela signifie que la saisie statique ne rend pas vraiment cela encombrant.J'ai retiré cela de http://www.mail-archive.com/[email protected]/msg02716.html, ce qui est une explication que j'ai écrite il y a plusieurs années.
J'utiliserai JavaScript dans cet exemple, mais de nombreuses autres langues fonctionneront également.
Notre objectif est de pouvoir écrire une fonction récursive de 1 variable en utilisant uniquement les fonctions de 1 variable et aucune affectation, en définissant les choses par leur nom, etc. sont donnés.) Semble impossible, hein? Par exemple, implémentons factorielle.
Eh bien, l'étape 1 est de dire que nous pourrions le faire facilement si nous trichions un peu. En utilisant les fonctions de 2 variables et l'affectation, nous pouvons au moins éviter d'avoir à utiliser l'affectation pour configurer la récursivité.
Voyons maintenant si nous pouvons moins tricher. Eh bien tout d'abord, nous utilisons l'affectation, mais nous n'en avons pas besoin. Nous pouvons simplement écrire X et Y en ligne.
Mais nous utilisons des fonctions de 2 variables pour obtenir une fonction de 1 variable. Pouvons-nous résoudre ce problème? Eh bien, un gars intelligent du nom de Haskell Curry a une astuce intéressante, si vous avez de bonnes fonctions d'ordre supérieur, vous n'avez besoin que de fonctions de 1 variable. La preuve est que vous pouvez passer de fonctions de 2 variables (ou plus dans le cas général) à 1 variable avec une transformation de texte purement mécanique comme celle-ci:
où ... reste exactement le même. (Cette astuce est appelée "currying" d'après son inventeur. Le langage Haskell est également nommé pour Haskell Curry. Fichier que sous trivia inutile.) Maintenant, appliquez simplement cette transformation partout et nous obtenons notre version finale.
N'hésitez pas à l'essayer. alert () qui reviennent, attachez-le à un bouton, peu importe. Ce code calcule les factorielles, récursivement, sans utiliser d'affectation, de déclarations ou de fonctions de 2 variables. (Mais essayer de tracer comment cela fonctionne est susceptible de faire tourner la tête. Et le remettre, sans dérivation, juste légèrement reformaté se traduira par un code qui est sûr de dérouter et de confondre.)
Vous pouvez remplacer les 4 lignes qui définissent récursivement factorielle par toute autre fonction récursive que vous souhaitez.
la source
function (n) { return builder(builder)(n);}
au lieu debuilder(builder)
?Je me demande s'il est utile d'essayer de construire cela à partir de zéro. Voyons voir. Voici une fonction factorielle récursive de base:
Refactorisons et créons une nouvelle fonction appelée
fact
qui renvoie une fonction de calcul factoriel anonyme au lieu d'effectuer le calcul lui-même:C'est un peu bizarre, mais il n'y a rien de mal à cela. Nous générons juste une nouvelle fonction factorielle à chaque étape.
La récursivité à ce stade est encore assez explicite. La
fact
fonction doit connaître son propre nom. Paramétrons l'appel récursif:C'est super, mais
recurser
il faut quand même connaître son propre nom. Paramétrons cela aussi:Maintenant, au lieu d'appeler
recurser(recurser)
directement, créons une fonction wrapper qui retourne son résultat:Nous pouvons maintenant nous débarrasser
recurser
complètement du nom; c'est juste un argument pour la fonction interne de Y, qui peut être remplacée par la fonction elle-même:Le seul nom externe encore référencé est
fact
, mais il devrait être clair maintenant que cela est également facilement paramétrable, créant la solution complète et générique:la source
recurser
. Pas la moindre idée de ce qu'il fait, ni pourquoi.recurser
fonction est la première étape vers cet objectif, car elle nous donne une version récursivefact
qui ne se référence jamais par son nom.function Y(recurse) { return recurse(recurse); } let factorial = Y(creator => value => { return value == 0 ? 1 : value * creator(creator)(value - 1); });
. Et voici comment je le digère (je ne sais pas si c'est correct): En ne référençant pas explicitement la fonction (non autorisée comme combinateur ), nous pouvons utiliser deux fonctions partiellement appliquées / curry (une fonction de créateur et la fonction de calcul), avec que nous pouvons créer des fonctions lambda / anonymes qui réalisent récursives sans avoir besoin d'un nom pour la fonction de calcul?La plupart des réponses ci - dessus décrivent ce que le Y-Combinator est , mais pas ce qu'elle est pour .
Des combinateurs à point fixe sont utilisés pour montrer que le calcul lambda est complet . C'est un résultat très important dans la théorie du calcul et fournit une base théorique pour la programmation fonctionnelle .
L'étude des combinateurs à virgule fixe m'a également aidé à vraiment comprendre la programmation fonctionnelle. Je n'ai cependant jamais trouvé d'utilité pour eux dans la programmation réelle.
la source
Y-combinator en JavaScript :
Edit : J'apprends beaucoup en regardant le code, mais celui-ci est un peu difficile à avaler sans arrière-plan - désolé. Avec quelques connaissances générales présentées par d'autres réponses, vous pouvez commencer à distinguer ce qui se passe.
La fonction Y est le "combinateur y". Jetez maintenant un œil à la
var factorial
ligne où Y est utilisé. Notez que vous lui passez une fonction qui a un paramètre (dans cet exemple,recurse
) qui est également utilisé plus tard dans la fonction interne. Le nom du paramètre devient essentiellement le nom de la fonction interne lui permettant d'effectuer un appel récursif (car il utiliserecurse()
dans sa définition.) Le combinateur y effectue la magie d'associer la fonction interne autrement anonyme au nom de paramètre de la fonction passée à Y.Pour l'explication complète de la façon dont Y fait la magie, consultez l' article lié (pas par moi d'ailleurs).
la source
arguments.callee
n'est pas autorisé en mode strict: developer.mozilla.org/en/JavaScript/…(function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
Pour les programmeurs qui n'ont pas rencontré de programmation fonctionnelle en profondeur, et ne se soucient pas de commencer maintenant, mais sont légèrement curieux:
Le combinateur Y est une formule qui vous permet d'implémenter la récursivité dans une situation où les fonctions ne peuvent pas avoir de noms mais peuvent être transmises comme arguments, utilisées comme valeurs de retour et définies dans d'autres fonctions.
Il fonctionne en se passant la fonction en tant qu'argument, afin de pouvoir s'appeler.
Cela fait partie du calcul lambda, qui est vraiment des mathématiques mais est en fait un langage de programmation, et est assez fondamental pour l'informatique et surtout pour la programmation fonctionnelle.
La valeur pratique quotidienne du combinateur Y est limitée, car les langages de programmation ont tendance à vous laisser nommer des fonctions.
Au cas où vous auriez besoin de l'identifier dans une composition policière, cela ressemble à ceci:
Vous pouvez généralement le repérer à cause des répétitions
(λx.f (x x))
.Les
λ
symboles sont la lettre grecque lambda, qui donne son nom au calcul lambda, et il y a beaucoup de(λx.t)
termes de style parce que c'est à cela que ressemble le calcul lambda.la source
U x = x x
,Y = U . (. U)
(abusant de la notation de type Haskell). OIEau, avec combinateurs appropriés,Y = BU(CBU)
. Ainsi,Yf = U (f . U) = (f . U) (f . U) = f (U (f . U)) = f ((f . U) (f . U))
.Récursivité anonyme
Un combinateur à virgule fixe est une fonction d'ordre supérieur
fix
qui, par définition, satisfait l'équivalencefix f
représente une solutionx
à l'équation à virgule fixeLa factorielle d'un nombre naturel peut être prouvée par
L'utilisation de
fix
preuves constructives arbitraires sur des fonctions générales / μ-récursives peut être dérivée sans auto-référentialité non-homogène.où
tel que
Cette preuve formelle que
utilise méthodiquement l'équivalence du combinateur à virgule fixe pour les réécritures
Calcul lambda
Le formalisme lambda calcul non typé consiste en une grammaire sans contexte
où
v
s'étend sur les variables, ainsi que les règles de réduction bêta et ETALa réduction bêta remplace toutes les occurrences libres de la variable
x
dans le corps d'abstraction («fonction»)B
par l'expression («argument»)E
. La réduction Eta élimine l'abstraction redondante. Il est parfois omis du formalisme. Une expression irréductible , à laquelle aucune règle de réduction ne s'applique, est sous forme normale ou canonique .est un raccourci pour
(abstraction multiarité),
est un raccourci pour
(application associative gauche),
et
sont équivalents alpha .
L'abstraction et l'application sont les deux seules «primitives de langage» du calcul lambda, mais elles permettent le codage de données et d'opérations arbitrairement complexes.
Les chiffres de l'Église sont un codage des nombres naturels similaires aux naturels peano-axiomatiques.
Une preuve formelle que
en utilisant la règle de réécriture de la réduction bêta:
Combinateurs
Dans le calcul lambda, les combinateurs sont des abstractions qui ne contiennent aucune variable libre. Plus simplement
I
:, le combinateur d'identitéisomorphe à la fonction d'identité
De tels combinateurs sont les opérateurs primitifs des calculs de combinateurs comme le système SKI.
La réduction bêta ne normalise pas fortement ; toutes les expressions réductibles, «redexes», convergent vers la forme normale sous réduction bêta. Un exemple simple est l'application divergente du
ω
combinateur omégaà lui-même:
La réduction des sous-expressions les plus à gauche («têtes») est prioritaire. L'ordre d'application normalise les arguments avant la substitution, contrairement à l' ordre normal . Les deux stratégies sont analogues à une évaluation enthousiaste, par exemple C, et à une évaluation paresseuse, par exemple Haskell.
diverge sous la réduction bêta de l'ordre d'application
car en sémantique stricte
mais converge sous une réduction bêta paresseuse d'ordre normal
Si une expression a une forme normale, une réduction bêta d'ordre normal la trouvera.
Oui
La propriété essentielle du
Y
combinateur à virgule fixeest donné par
L'équivalence
est isomorphe à
Le calcul lambda non typé peut coder des preuves constructives arbitraires sur des fonctions générales / μ-récursives.
(Multiplication retardée, confluence)
Pour le calcul lambda non typé de l'Église, il a été démontré qu'il existe en outre une infinité récursivement énumérable de combinateurs à virgule fixe
Y
.La réduction bêta d'ordre normal fait du calcul lambda non étendu non étendu un système de réécriture complet de Turing.
À Haskell, le combinateur à virgule fixe peut être mis en œuvre avec élégance
La paresse de Haskell se normalise à une fin avant que toutes les sous-expressions aient été évaluées.
la source
λ x . x
, comment allez-vous aujourd'hui?D'autres réponses fournissent une réponse assez concise à cela, sans un fait important: vous n'avez pas besoin d'implémenter un combinateur à point fixe dans un langage pratique de cette manière compliquée et cela ne sert à rien (sauf "regardez, je sais ce que le combinateur Y") est"). C'est un concept théorique important, mais de peu de valeur pratique.
la source
Voici une implémentation JavaScript du Y-Combinator et de la fonction factorielle (de l'article de Douglas Crockford, disponible à: http://javascript.crockford.com/little.html ).
la source
Un Y-Combinator est un autre nom pour un condensateur de flux.
la source
J'ai écrit une sorte de "guide idiots" sur le Y-Combinator à la fois dans Clojure et Scheme afin de m'aider à y faire face. Ils sont influencés par le matériel de "The Little Schemer"
Dans le schéma: https://gist.github.com/z5h/238891
ou Clojure: https://gist.github.com/z5h/5102747
Les deux tutoriels sont du code entrecoupé de commentaires et doivent être coupés et collables dans votre éditeur préféré.
la source
En tant que débutant dans les combinateurs, j'ai trouvé l'article de Mike Vanier (merci Nicholas Mancuso) très utile. Je voudrais écrire un résumé, en plus de documenter ma compréhension, si cela pouvait être utile à d'autres, je serais très heureux.
De Crappy à Less Crappy
En utilisant la factorielle comme exemple, nous utilisons la
almost-factorial
fonction suivante pour calculer la factorielle du nombrex
:Dans le pseudo-code ci-dessus,
almost-factorial
prend la fonctionf
et le nombrex
(almost-factorial
est curry, il peut donc être considéré comme prenant la fonctionf
et renvoyant une fonction à 1 entité).Lors du
almost-factorial
calcul de la factorielle pourx
, il délègue le calcul de la factorielle pourx - 1
fonctionnerf
et accumule ce résultat avecx
(dans ce cas, il multiplie le résultat de (x - 1) par x).Il peut être vu comme
almost-factorial
prend une version merdique de la fonction factorielle (qui ne peut calculer que le nombre de tillx - 1
) et renvoie une version moins merdique de la factorielle (qui calcule le nombre de tillx
). Comme sous cette forme:Si nous passons à plusieurs reprises la version moins merdique de factorielle à
almost-factorial
, nous obtiendrons finalement notre fonction factorielle souhaitéef
. Où il peut être considéré comme:Fix-point
Le fait que cela
almost-factorial f = f
signifief
est le point fixe de la fonctionalmost-factorial
.C'était une façon vraiment intéressante de voir les relations des fonctions ci-dessus et c'était un moment aha pour moi. (veuillez lire le post de Mike sur fix-point si vous ne l'avez pas fait)
Trois fonctions
Pour généraliser, nous avons un non récursif fonction
fn
(comme notre presque factoriel), nous avons le point fixe fonctionfr
(comme notre f), alors qu'est - ce qu'ilY
fait est quand on donneY
fn
,Y
retourne la fonction point fixe defn
.Donc en résumé (simplifié en supposant
fr
ne prend qu'un seul paramètre;x
dégénère enx - 1
,x - 2
... en récursivité):fn
:def fn fr x = ...accumulate x with result from (fr (- x 1))
c'est la fonction presque utile - bien que nous ne puissions pas l'utiliserfn
directementx
, elle sera utile très bientôt. Ce non récursiffn
utilise une fonctionfr
pour calculer son résultatfn fr = fr
,fr
Est le point fixe defn
,fr
est l' utile funciton, nous pouvons utiliserfr
surx
pour obtenir notre résultatY fn = fr
,Y
Retourne le point fixe d'une fonction,Y
transforme notre presque utile fonctionfn
dans utilefr
Dérivation
Y
(non inclus)Je vais sauter la dérivation de
Y
et aller à la compréhensionY
. Le message de Mike Vainer contient beaucoup de détails.La forme de
Y
Y
est défini comme (au format lambda calcul ):Si nous remplaçons la variable
s
à gauche des fonctions, nous obtenonsDonc, en effet, le résultat de
(Y f)
est le point fixe def
.Pourquoi ça
(Y f)
marche?Selon la signature de
f
,(Y f)
peut être une fonction de n'importe quelle arité, pour simplifier, supposons(Y f)
que ne prend qu'un paramètre, comme notre fonction factorielle.depuis
fn fr = fr
, nous continuonsle calcul récursif se termine lorsque le plus interne
(fn fr 1)
est le cas de base etfn
ne l'utilise pasfr
dans le calcul.En regardant à
Y
nouveau:Donc
Pour moi, les parties magiques de cette configuration sont:
fn
etfr
interdépendants les uns des autres:fr
«s'enroule»fn
à l' intérieur, chaque fois qu'ilfr
est utilisé pour calculerx
, il «apparaît» («se soulève»?) anfn
et délègue le calcul à celafn
(en passant en luifr
- même etx
); d'autre part,fn
dépendfr
et utilisefr
pour calculer le résultat d'un problème plus petitx-1
.fr
on utilise pour définirfn
(quand on l'fn
utilisefr
dans ses opérations), le réelfr
n'est pas encore défini.fn
qui définit la véritable logique métier. Sur la basefn
,Y
créefr
- une fonction d'assistance sous une forme spécifique - pour faciliter le calcul pourfn
une récursif manière.Cela m'a aidé à comprendre
Y
cette façon pour le moment, j'espère que cela aide.BTW, j'ai également trouvé le livre An Introduction to Functional Programming Through Lambda Calculus très bon, je ne suis que partiellement à travers et le fait que je n'ai pas pu me mettre la tête
Y
dans le livre m'a conduit à ce poste.la source
Voici les réponses aux questions originales , compilées à partir de l'article (qui vaut TOTALEMENT la peine d'être lu) mentionné dans la réponse de Nicholas Mancuso , ainsi que d'autres réponses:
Un Y-combinator est une "fonctionnelle" (ou une fonction d'ordre supérieur - une fonction qui opère sur d'autres fonctions) qui prend un seul argument, qui est une fonction qui n'est pas récursive, et renvoie une version de la fonction qui est récursif.
Un peu récursif =), mais définition plus approfondie:
Un combinateur - est juste une expression lambda sans variable libre.
Variable libre - est une variable qui n'est pas une variable liée.
Variable liée - variable qui est contenue dans le corps d'une expression lambda qui a ce nom de variable comme l'un de ses arguments.
Une autre façon de penser à cela est que le combinateur est une telle expression lambda, dans laquelle vous pouvez remplacer le nom d'un combinateur avec sa définition partout où il se trouve et que tout fonctionne toujours (vous entrerez dans une boucle infinie si le combinateur contiennent une référence à lui-même, à l'intérieur du corps lambda).
Le combinateur Y est un combinateur à virgule fixe.
Le point fixe d'une fonction est un élément du domaine de la fonction qui est mappé sur lui-même par la fonction.
C'est-à-dire,
c
est un point fixe de la fonctionf(x)
sif(c) = c
cela signifie
f(f(...f(c)...)) = fn(c) = c
Les exemples ci-dessous supposent un typage fort + dynamique :
Combinateur Y paresseux (ordre normal):
Cette définition s'applique aux langues avec évaluation paresseuse (également: différée, appel par besoin) - stratégie d'évaluation qui retarde l'évaluation d'une expression jusqu'à ce que sa valeur soit nécessaire.
Cela signifie que, pour une fonction donnée
f
(qui est une fonction non récursive), la fonction récursive correspondante peut être obtenue d'abord en calculantλx.f(x x)
, puis en appliquant cette expression lambda à elle-même.Combinateur Y strict (ordre applicatif):
Cette définition s'applique aux langues avec une évaluation stricte (aussi: avide, gourmande) - stratégie d'évaluation dans laquelle une expression est évaluée dès qu'elle est liée à une variable.
Il est identique à un paresseux dans sa nature, il a juste un
λ
emballage supplémentaire pour retarder l'évaluation du corps du lambda. J'ai posé une autre question , quelque peu liée à ce sujet.Voléemprunté à la réponse de Chris Ammerman : Y-combinator généralise la récursivité, en en faisant abstraction, et en la séparant ainsi du travail réel de la fonction en question.Même si Y-combinator a des applications pratiques, il s'agit principalement d'un concept théorique dont la compréhension élargira votre vision globale et augmentera probablement vos compétences analytiques et de développement.
Comme l'a déclaré Mike Vanier : il est possible de définir un combinateur Y dans de nombreux langages typés statiquement, mais (au moins dans les exemples que j'ai vus), de telles définitions nécessitent généralement un piratage de type non évident, car le combinateur Y lui-même ne fait pas '' t ont un type statique simple. Cela dépasse le cadre de cet article, donc je ne le mentionnerai pas plus loin
Et comme mentionné par Chris Ammerman : la plupart des langages procéduraux ont un typage statique.
Alors répondez à celui-ci - pas vraiment.
la source
Le combinateur y implémente la récursivité anonyme. Donc au lieu de
tu peux faire
bien sûr, le combinateur y ne fonctionne que dans les langues appelées par nom. Si vous souhaitez utiliser ceci dans n'importe quel langage d'appel normal, alors vous aurez besoin du combinateur z associé (le combinateur y divergent / boucle infinie).
la source
Un combinateur à virgule fixe (ou opérateur à virgule fixe) est une fonction d'ordre supérieur qui calcule un point fixe d'autres fonctions. Cette opération est pertinente dans la théorie du langage de programmation car elle permet l'implémentation de la récursivité sous la forme d'une règle de réécriture, sans prise en charge explicite du moteur d'exécution du langage. (src Wikipedia)
la source
Cet opérateur peut vous simplifier la vie:
Ensuite, vous évitez la fonction supplémentaire:
Enfin, vous appelez
fac(5)
.la source
Je pense que la meilleure façon d'y répondre est de choisir une langue, comme JavaScript:
Maintenant, réécrivez-le pour qu'il n'utilise pas le nom de la fonction à l'intérieur de la fonction, mais l'appelle toujours récursivement.
Le seul endroit où le nom de la fonction
factorial
doit être vu est sur le site de l'appel.Astuce: vous ne pouvez pas utiliser de noms de fonctions, mais vous pouvez utiliser des noms de paramètres.
Travaillez le problème. Ne cherchez pas. Une fois que vous l'aurez résolu, vous comprendrez quel problème le combinateur y résout.
la source