Fiquei um pouco confuso com a documentação do fix
(embora ache que entenda o que ela deve fazer agora), então olhei o código-fonte. Isso me deixou mais confuso:
fix :: (a -> a) -> a
fix f = let x = f x in x
Como exatamente isso retorna um ponto fixo?
Decidi experimentar na linha de comando:
Prelude Data.Function> fix id
...
E fica lá. Agora, para ser justo, isso está no meu antigo macbook, que é meio lento. No entanto, essa função não pode ser muito cara do ponto de vista computacional, uma vez que qualquer coisa passada para o id retorna a mesma coisa (sem mencionar que não está consumindo tempo da CPU). O que estou fazendo errado?
haskell
fixpoint-combinators
letrec
Jason Baker
fonte
fonte
fix error
ghci e se sentir bem consigo mesmo".fix
comofix f = f (fix f)
. Curto, simples, funciona e idêntico à definição matemática.fix (1:) !! (10^8)
,. O original faz isso em memória constante, o seu usa memória linear (o que o torna um pouco mais lento também). Ou seja, usar o let "dá um nó mais forte" e permite que uma estrutura de dados circular seja gerada, enquanto a sua não.fix
! me ajudou a entenderfix
muito.Respostas:
Você não está fazendo nada errado.
fix id
é um loop infinito.Quando dizemos que
fix
retorna o menor ponto fixo de uma função, queremos dizer isso no sentido da teoria do domínio . Entãofix (\x -> 2*x-1)
é não vai voltar1
, porque embora1
é um ponto fixo dessa função, não é o menos um na ordenação de domínio.Não posso descrever a ordenação do domínio em um ou dois parágrafos, então vou encaminhá-lo ao link de teoria do domínio acima. É um excelente tutorial, fácil de ler e bastante esclarecedor. Eu recomendo.
Para a visualização de 10.000 pés,
fix
é uma função de ordem superior que codifica a ideia de recursão . Se você tiver a expressão:let x = 1:x in x
O que resulta na lista infinita
[1,1..]
, você poderia dizer a mesma coisa usandofix
:fix (\x -> 1:x)
(Ou simplesmente
fix (1:)
), que diz encontre-me um ponto fixo da(1:)
função, IOW um valorx
tal quex = 1:x
... exatamente como definimos acima. Como você pode ver pela definição,fix
nada mais é do que essa ideia - recursão encapsulada em uma função.É um conceito verdadeiramente geral de recursão também - você pode escrever qualquer função recursiva dessa forma, incluindo funções que usam recursão polimórfica . Por exemplo, a função típica de fibonacci:
fib n = if n < 2 then n else fib (n-1) + fib (n-2)
Pode ser escrito da
fix
seguinte maneira:fib = fix (\f -> \n -> if n < 2 then n else f (n-1) + f (n-2))
Exercício: expanda a definição de
fix
para mostrar que essas duas definições defib
são equivalentes.Mas para uma compreensão completa, leia sobre a teoria do domínio. É uma coisa muito legal.
fonte
fix id
:fix
pega uma função do tipoa -> a
e retorna um valor do tipoa
. Porid
ser polimórfico para qualquera
,fix id
terá o tipoa
, ou seja, qualquer valor possível. Em Haskell, o único valor que pode ser de qualquer tipo é bottom, ⊥, e é indistinguível de um cálculo sem terminação. Portanto,fix id
produz exatamente o que deveria, o valor inferior. Um perigo defix
é que se ⊥ for um ponto fixo de sua função, então é por definição o ponto menos fixo, portantofix
não terminará.undefined
também é um valor de qualquer tipo. Você pode definirfix
como:fix f = foldr (\_ -> f) undefined (repeat undefined)
._Y f = f (_Y f)
.Eu não alego entender isso de forma alguma, mas se isso ajudar alguém ... então uau.
Considere a definição de
fix
.fix f = let x = f x in x
. A parte incompreensível é quex
é definido comof x
. Mas pense nisso por um minuto.x = f x
Como x = fx, então podemos substituir o valor de
x
no lado direito disso, certo? Portanto ...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.
Portanto, o truque é, para encerrar,
f
deve gerar algum tipo de estrutura, de modo que umf
padrão posterior possa corresponder a essa estrutura e encerrar a recursão, sem realmente se preocupar com o "valor" completo de seu parâmetro (?)A menos, é claro, que você queira fazer algo como criar uma lista infinita, como ilustrou luqui.
A explicação fatorial de TomMD é boa. A assinatura de tipo de Fix é
(a -> a) -> a
. A assinatura de tipo para(\recurse d -> if d > 0 then d * (recurse (d-1)) else 1)
é(b -> b) -> b -> b
, em outras palavras(b -> b) -> (b -> b)
,. Então podemos dizer issoa = (b -> b)
. Dessa forma, fix pega nossa função, que éa -> a
, ou realmente,(b -> b) -> (b -> b)
e vai retornar um resultado do tipoa
, ou seja,b -> b
ou seja, outra função!Espere, pensei que deveria retornar um ponto fixo ... não uma função. Bem, é verdade, mais ou menos (já que funções são dados). Você pode imaginar que nos deu a função definitiva de encontrar um fatorial. Demos a ela uma função que não sabe como recursar (portanto, um dos parâmetros para ela é uma função usada para recursar), e
fix
ensinamos como recursar.Lembra que eu disse que isso
f
tem que gerar algum tipo de estrutura para que umf
padrão posterior possa combinar e terminar? Bem, isso não é exatamente certo, eu acho. TomMD ilustrou como podemos expandirx
para aplicar a função e avançar em direção ao caso base. Para sua função, ele usou um if / then, e é isso que causa a rescisão. Após substituições repetidas, ain
parte de toda a definição defix
eventualmente deixa de ser definida em termos dex
e é quando é computável e completa.fonte
Você precisa de uma maneira para o ponto de fixação terminar. Expandindo seu exemplo, é óbvio que não vai terminar:
fix id --> let x = id x in x --> id x --> id (id x) --> id (id (id x)) --> ...
Aqui está um exemplo real de como eu uso correção (observe que não uso correção com frequência e provavelmente estava cansado / não me preocupei com código legível quando escrevi isso):
(fix (\f h -> if (pred h) then f (mutate h) else h)) q
WTF, você diz! Bem, sim, mas existem alguns pontos realmente úteis aqui. Em primeiro lugar, seu primeiro
fix
argumento geralmente deve ser uma função que é o caso de 'recurse' e o segundo argumento são os dados sobre os quais agir. Aqui está o mesmo código de uma função nomeada:getQ h | pred h = getQ (mutate h) | otherwise = h
Se você ainda está confuso, talvez o fatorial seja um exemplo mais fácil:
fix (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) 5 -->* 120
Observe a avaliação:
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, você acabou de ver isso? Isso
x
se tornou uma função dentro do nossothen
ramo.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 -->
No exemplo acima, você precisa se lembrar
x = f x
, daí os dois argumentos dex 2
no final em vez de apenas2
.let x = ... in 3 * (\d -> if d > 0 then d * (x (d-1)) else 1) 2 -->
E vou parar por aqui!
fonte
fix
sentido para mim. Minha resposta depende muito do que você já disse.id x
apenas reduz ax
(que então reduz novamente aid x
). - Então, na 2ª amostra (fact
), quando ax
conversão é forçada pela primeira vez, o valor resultante é lembrado e reutilizado. O recálculo de(\recurse ...) x
aconteceria com a definição dey g = g (y g)
não compartilhamento , não com esta definição de compartilhamentofix
. - Fiz a edição de teste aqui - você pode usá-la ou posso fazer a edição, se você aprovar.fix id
é reduzido,let x = id x in x
também força o valor do aplicativoid x
dentro dolet
quadro (conversão), então ele reduz paralet x = x in x
, e isso faz um loop. Parece que sim.fix
e Y é muito clara e importante em Haskell. Não vejo o que adianta mostrar a ordem de redução errada quando a correta é ainda mais curta, muito mais clara e fácil de seguir e reflete corretamente o que realmente está acontecendo.Pelo que entendi, ele encontra um valor para a função, de modo que produza a mesma coisa que você deu a ele. O problema é que ele sempre escolherá indefinido (ou um loop infinito, em haskell, os loops indefinidos e infinitos são o mesmo) ou o que quer que tenha mais indefinidos. Por exemplo, com id,
λ <*Main Data.Function>: id undefined *** Exception: Prelude.undefined
Como você pode ver, indefinido é um ponto fixo, então
fix
o escolheremos. Em vez disso, faça (\ x-> 1: x).λ <*Main Data.Function>: undefined *** Exception: Prelude.undefined λ <*Main Data.Function>: (\x->1:x) undefined [1*** Exception: Prelude.undefined
Portanto,
fix
não pode escolher indefinido. Para torná-lo um pouco mais conectado a loops infinitos.λ <*Main Data.Function>: let y=y in y ^CInterrupted. λ <*Main Data.Function>: (\x->1:x) (let y=y in y) [1^CInterrupted.
Novamente, uma pequena diferença. Então, qual é o ponto fixo? Vamos tentar
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
É o mesmo! Uma vez que este é o único ponto fixo,
fix
deve se estabelecer nele. Desculpefix
, sem loops infinitos ou indefinidos para você.fonte