Comment utiliser Fix et comment ça marche?

87

J'étais un peu confus par la documentation de fix(même si je pense comprendre ce qu'il est censé faire maintenant), alors j'ai regardé le code source. Cela m'a laissé plus confus:

fix :: (a -> a) -> a
fix f = let x = f x in x

Comment cela renvoie-t-il exactement un point fixe?

J'ai décidé de l'essayer en ligne de commande:

Prelude Data.Function> fix id
...

Et ça tient là. Maintenant, pour être honnête, c'est sur mon ancien macbook qui est un peu lent. Cependant, cette fonction ne peut pas être trop coûteuse en calcul car tout ce qui est passé à id rend la même chose (sans parler du fait qu'elle ne consomme pas de temps CPU). Qu'est-ce que je fais mal?

Jason Baker
la source
68
La réponse farce est "le correctif n'a aucune utilité réelle, c'est juste là pour que vous puissiez taper fix errorghci et vous sentir bien dans votre peau."
Thomas M. DuBuisson
3
@TomMD - Drôle. Je me souviendrai que si quelqu'un me demande ce que fait la solution et que je me sens désagréable. :-)
Jason Baker
2
J'écris habituellement fixcomme fix f = f (fix f). Court, simple, fonctionne et identique à la définition mathématique.
newacct le
20
@newacct, oui c'est comme ça que j'y pense aussi. Mais celui-ci peut conduire à des structures plus efficaces. Vous pouvez voir la différence si vous évaluez, par exemple, fix (1:) !! (10^8). L'original le fait en mémoire constante, le vôtre prend de la mémoire linéaire (ce qui le rend un peu plus lent aussi). Autrement dit, l'utilisation du let "fait un nœud plus serré" et permet de générer une structure de données circulaire, contrairement à la vôtre.
luqui
22
Vous auriez pu fixaussi réinventer ! m'a fixbeaucoup aidé à comprendre .
fredoverflow

Réponses:

90

Vous ne faites rien de mal. fix idest une boucle infinie.

Lorsque nous disons que fixrenvoie le point le moins fixe d'une fonction, nous l'entendons au sens de la théorie des domaines . Alors , fix (\x -> 2*x-1)est ne va revenir 1, parce que même si 1est un point fixe de cette fonction, il est pas moins un dans la commande de domaine.

Je ne peux pas décrire l'ordre des domaines en un ou deux paragraphes, je vous renvoie donc au lien de théorie des domaines ci-dessus. C'est un excellent tutoriel, facile à lire et assez instructif. Je le recommande fortement.

Pour la vue à 10 000 pieds, fixest une fonction d'ordre supérieur qui encode l'idée de récursivité . Si vous avez l'expression:

let x = 1:x in x

Ce qui aboutit à la liste infinie [1,1..], vous pourriez dire la même chose en utilisant fix:

fix (\x -> 1:x)

(Ou simplement fix (1:)), qui dit me trouver un point fixe de la (1:)fonction, IOW une valeur xtelle que x = 1:x... tout comme nous l'avons défini ci-dessus. Comme vous pouvez le voir dans la définition, il fixn'y a rien de plus que cette idée - la récursivité encapsulée dans une fonction.

C'est aussi un concept vraiment général de récursivité - vous pouvez écrire n'importe quelle fonction récursive de cette façon, y compris les fonctions qui utilisent la récursivité polymorphe . Ainsi, par exemple, la fonction typique de fibonacci:

fib n = if n < 2 then n else fib (n-1) + fib (n-2)

Peut être écrit de fixcette façon:

fib = fix (\f -> \n -> if n < 2 then n else f (n-1) + f (n-2))

Exercice: élargissez la définition de fixpour montrer que ces deux définitions de fibsont équivalentes.

Mais pour une compréhension complète, lisez la théorie des domaines. C'est vraiment cool.

Luqui
la source
32
Voici une façon de penser connexe fix id: fixprend une fonction de type a -> aet renvoie une valeur de type a. Parce qu'il idest polymorphe pour tout a, fix idaura le type a, c'est-à-dire toute valeur possible. Dans Haskell, la seule valeur qui peut être de n'importe quel type est bottom, ⊥, et ne se distingue pas d'un calcul sans terminaison. Donc fix idproduit exactement ce qu'il devrait, la valeur inférieure. Un danger de fixest que si ⊥ est un point fixe de votre fonction, alors c'est par définition le point le moins fixe, donc fixil ne se terminera pas.
John L
4
@JohnL dans Haskell undefinedest également une valeur de tout type. Vous pouvez définir fixcomme: fix f = foldr (\_ -> f) undefined (repeat undefined).
didest
1
@Diego votre code équivaut à _Y f = f (_Y f).
Will Ness
25

Je ne prétends pas du tout comprendre cela, mais si cela aide quelqu'un ... alors oui.

Considérez la définition de fix. fix f = let x = f x in x. La partie ahurissante est qu'elle xest définie comme f x. Mais pensez-y une minute.

x = f x

Puisque x = fx, alors nous pouvons substituer la valeur de xsur le côté droit de cela, non? Ainsi donc...

x = f . f $ x -- or x = f (f x)
x = f . f . f $ x -- or x = f (f (f x))
x = f . f . f . f . f . f . f . f . f . f . f $ x -- etc.

Donc, l'astuce est, pour se terminer, fdoit générer une sorte de structure, de sorte qu'un fmodèle ultérieur puisse correspondre à cette structure et terminer la récursivité, sans se soucier réellement de la "valeur" complète de son paramètre (?)

À moins, bien sûr, que vous ne souhaitiez faire quelque chose comme créer une liste infinie, comme luqui l'a illustré.

L'explication factorielle de TomMD est bonne. La signature de type du correctif est (a -> a) -> a. La signature de type pour (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1)est (b -> b) -> b -> b, autrement dit, (b -> b) -> (b -> b). Alors on peut dire ça a = (b -> b). De cette façon, fix prend notre fonction, qui est a -> a, ou vraiment, (b -> b) -> (b -> b)et retournera un résultat de type a, en d'autres termes b -> b, en d'autres termes, une autre fonction!

Attendez, je pensais que c'était censé renvoyer un point fixe ... pas une fonction. Eh bien c'est le cas, en quelque sorte (puisque les fonctions sont des données). Vous pouvez imaginer que cela nous a donné la fonction définitive pour trouver une factorielle. Nous lui avons donné une fonction qui ne sait pas comment récurer (par conséquent, l'un de ses paramètres est une fonction utilisée pour récurer) et lui avons fixappris à récurer.

Rappelez-vous comment j'ai dit que cela fdevait générer une sorte de structure pour qu'un fmodèle ultérieur puisse correspondre et se terminer? Eh bien, ce n'est pas tout à fait vrai, je suppose. TomMD a illustré comment nous pouvons développer xpour appliquer la fonction et avancer vers le cas de base. Pour sa fonction, il a utilisé un si / alors, et c'est ce qui cause la résiliation. Après des remplacements répétés, la inpartie de la définition entière de fixcesse finalement d'être définie en termes de xet c'est à ce moment-là qu'elle est calculable et complète.

Dan Burton
la source
Merci. C'est une explication très utile et pratique.
kizzx2
17

Vous avez besoin d'un moyen pour que le point fixe se termine. En élargissant votre exemple, il est évident que cela ne se terminera pas:

fix id
--> let x = id x in x
--> id x
--> id (id x)
--> id (id (id x))
--> ...

Voici un exemple réel de mon utilisation de correctif (notez que je n'utilise pas souvent le correctif et que j'étais probablement fatigué / ne m'inquiétait pas du code lisible lorsque j'ai écrit ceci):

(fix (\f h -> if (pred h) then f (mutate h) else h)) q

WTF, dites-vous! Eh bien, oui, mais il y a quelques points vraiment utiles ici. Tout d'abord, votre premier fixargument devrait généralement être une fonction qui est le cas «récurer» et le deuxième argument est les données sur lesquelles agir. Voici le même code qu'une fonction nommée:

getQ h
      | pred h = getQ (mutate h)
      | otherwise = h

Si vous êtes toujours confus, la factorielle sera peut-être un exemple plus simple:

fix (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) 5 -->* 120

Notez l'évaluation:

fix (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) 3 -->
let x = (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) x in x 3 -->
let x = ... in (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) x 3 -->
let x = ... in (\d -> if d > 0 then d * (x (d-1)) else 1) 3

Oh, tu viens de voir ça? C'est xdevenu une fonction au sein de notre thensuccursale.

let x = ... in if 3 > 0 then 3 * (x (3 - 1)) else 1) -->
let x = ... in 3 * x 2 -->
let x = ... in 3 * (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) x 2 -->

Dans ce qui précède, vous devez vous rappeler x = f x, d'où les deux arguments de x 2à la fin au lieu de juste 2.

let x = ... in 3 * (\d -> if d > 0 then d * (x (d-1)) else 1) 2 -->

Et je vais m'arrêter ici!

Thomas M. DuBuisson
la source
Votre réponse est ce qui a vraiment du fixsens pour moi. Ma réponse dépend en grande partie de ce que vous avez déjà dit.
Dan Burton
@Thomas vos deux séquences de réduction sont incorrectes. :) id xse réduit simplement à x(qui se réduit ensuite à id x). - Ensuite, dans le 2ème échantillon ( fact), lorsque le xthunk est d'abord forcé, la valeur résultante est mémorisée et réutilisée. Le recalcul de (\recurse ...) xse produirait avec une définition de non-partagey g = g (y g) , pas avec cette définition de partagefix . - J'ai effectué la modification d'essai ici - vous pouvez l'utiliser, ou je pourrais effectuer la modification si vous l'approuvez.
Will Ness
en fait, quand fix idest réduit, let x = id x in xforce également la valeur de l'application id xà l'intérieur du letcadre (thunk), donc il se réduit à let x = x in x, et cela boucle. On dirait ça.
Will Ness
Correct. Ma réponse utilise le raisonnement équationnel. Montrer la réduction à la Haskell, qui se préoccupe de l'ordre d'évaluation, ne sert qu'à confondre l'exemple sans véritable gain.
Thomas M. DuBuisson
1
La question est étiquetée à la fois avec haskell et letrec (c'est-à-dire le let récursif, avec partage). La distinction entre fixet Y est très claire et importante chez Haskell. Je ne vois pas à quoi sert d'afficher le mauvais ordre de réduction alors que le bon est encore plus court, beaucoup plus clair et plus facile à suivre, et reflète correctement ce qui se passe réellement.
Will Ness
3

D'après ce que je comprends, il trouve une valeur pour la fonction, de sorte qu'elle génère la même chose que vous lui donnez. Le hic, c'est qu'il choisira toujours undefined (ou une boucle infinie, dans haskell, les boucles indéfinies et infinies sont les mêmes) ou tout ce qui contient le plus d'indéfini. Par exemple, avec id,

λ <*Main Data.Function>: id undefined
*** Exception: Prelude.undefined

Comme vous pouvez le voir, undefined est un point fixe, fixnous le choisirons donc. Si vous faites plutôt (\ x-> 1: x).

λ <*Main Data.Function>: undefined
*** Exception: Prelude.undefined
λ <*Main Data.Function>: (\x->1:x) undefined
[1*** Exception: Prelude.undefined

Donc, fixje ne peux pas choisir undefined. Pour le rendre un peu plus connecté à des boucles infinies.

λ <*Main Data.Function>: let y=y in y
^CInterrupted.
λ <*Main Data.Function>: (\x->1:x) (let y=y in y)
[1^CInterrupted.

Encore une fois, une légère différence. Alors, quel est le point fixe? Essayons repeat 1.

λ <*Main Data.Function>: repeat 1
[1,1,1,1,1,1, and so on
λ <*Main Data.Function>: (\x->1:x) $ repeat 1
[1,1,1,1,1,1, and so on

C'est le même! Puisque c'est le seul point fixe, il fixfaut s'y arrêter. Désolé fix, pas de boucles infinies ou indéfinies pour vous.

PyRulez
la source