Implementar zip usando foldr
-
04-07-2019 - |
Pergunta
Atualmente estou no capítulo 4 do Real World Haskell, e eu estou tentando envolver minha cabeça em torno de implementação foldl em termos de foldr .
(Aqui está o seu código:)
myFoldl :: (a -> b -> a) -> a -> [b] -> a
myFoldl f z xs = foldr step id xs z
where step x g a = g (f a x)
Eu pensei em tentar implementar zip
usando a mesma técnica, mas eu não parecem estar fazendo algum progresso. Será que é mesmo possível?
Solução
zip2 xs ys = foldr step done xs ys
where done ys = []
step x zipsfn [] = []
step x zipsfn (y:ys) = (x, y) : (zipsfn ys)
Como isso funciona: (passo foldr xs feito) retorna uma função que consome ys; assim que nós vamos para baixo na lista xs construção de uma composição aninhada de funções que cada um deles será aplicada à parte correspondente do ys.
Como chegar a ele: Eu comecei com a idéia geral (de semelhante exemplos visto antes), escreveu
zip2 xs ys = foldr step done xs ys
, em seguida, preenchido em cada uma das seguintes linhas, por sua vez, com o que tinha a ser para fazer os tipos e valores saem direito. Era mais fácil de considerar os casos mais simples antes de as mais difíceis.
A primeira linha poderia ser escrito mais simplesmente como
zip2 = foldr step done
como mattiast mostrou.
Outras dicas
A resposta já tinha sido dada aqui, mas não uma derivação (ilustrativa). Assim, mesmo depois de todos esses anos, talvez vale a pena adicioná-lo.
É realmente muito simples. Primeiro,
foldr f z xs = foldr f z [x1,x2,x3,...,xn] = f x1 (foldr f z [x2,x3,...,xn]) = ... = f x1 (f x2 (f x3 (... (f xn z) ...)))
, portanto, por eta-expansão,
foldr f z xs ys = foldr f z [x1,x2,x3,...,xn] ys = f x1 (foldr f z [x2,x3,...,xn]) ys = ... = f x1 (f x2 (f x3 (... (f xn z) ...))) ys
Como é evidente aqui, se f
é não forçar na sua 2ª argumento , ele começa a trabalhar início em x1
e ys
, f x1
r1
ys
onde r1 =
(f x2 (f x3 (... (f xn z) ...)))
= foldr f z [x2,x3,...,xn]
.
Assim, usando
f x1 r1 [] = [] f x1 r1 (y1:ys1) = (x1,y1) : r1 ys1
nós arranjamos para a passagem de informações da esquerda para a direita ??em> ao longo da lista, por chamando r1
com o descansar da lista de entrada ys1
, foldr f z [x2,x3,...,xn]
ys1 = f x2
r2
ys1
, como o próximo passo. E é isso.
Quando ys
é mais curto do que xs
(ou o mesmo comprimento), o caso []
para incêndios f
e o processamento pára. Mas se ys
é maior do que xs
seguida, caso f
de []
não fogo e nós vamos chegar ao f xn
final z
aplicação (yn:ysn)
,
f xn z (yn:ysn) = (xn,yn) : z ysn
Uma vez que chegamos ao final de xs
, o zip
processamento de parada obrigatória:
z _ = []
E isso significa que o z = const []
definição deve ser usado:
zip xs ys = foldr f (const []) xs ys
where
f x r [] = []
f x r (y:ys) = (x,y) : r ys
Do ponto de vista f
, r
desempenha o papel de um continuação sucesso , que f
chamadas quando o processamento é para continuar, depois de ter emitido o (x,y)
par.
Assim r
é "o que é feito com mais ys
quando há mais x
s" , e z = const []
, o nil
-Case em foldr
, é "o que é feito com ys
quando não há mais x
s ". Ou f
pode parar por si só, retornando []
quando ys
está esgotado.
Observe como ys
é usado como um tipo de valor acumulando, que é passado da esquerda para a direita ao longo da lista xs
, de uma invocação de f
para o próximo ( "acumular" ser passo, aqui, tirando um elemento cabeça dele ).
Naturalmente Isto corresponde à esquerda dobra, onde um passo de acumulação é "aplicar a função", com z = id
retornando o valor final acumulada quando "não há mais x
s":
foldl f a xs =~ foldr (\x r a-> r (f a x)) id xs a
Da mesma forma, para as listas finitas,
foldr f a xs =~ foldl (\r x a-> r (f x a)) id xs a
E uma vez que a função combinando começa a decidir se quer continuar ou não, agora é possível ter esquerdo dobrar que podem parar mais cedo:
foldlWhile t f a xs = foldr cons id xs a
where
cons x r a = if t x then r (f a x) else a
ou um pular esquerda dobra, foldlWhen t ...
, com
cons x r a = if t x then r (f a x) else r a
etc.
Eu encontrei uma maneira usando método bastante semelhante à sua:
myzip = foldr step (const []) :: [a] -> [b] -> [(a,b)]
where step a f (b:bs) = (a,b):(f bs)
step a f [] = []
Para os Haskellers não-nativos aqui, eu escrevi uma versão Esquema deste algoritmo para tornar mais claro o que está realmente acontecendo:
> (define (zip lista listb)
((foldr (lambda (el func)
(lambda (a)
(if (empty? a)
empty
(cons (cons el (first a)) (func (rest a))))))
(lambda (a) empty)
lista) listb))
> (zip '(1 2 3 4) '(5 6 7 8))
(list (cons 1 5) (cons 2 6) (cons 3 7) (cons 4 8))
Os resultados foldr
em uma função que, quando aplicado a uma lista, irá devolver o fecho de correr da lista dobrada com a lista dada para a função. O Haskell esconde a lambda
interna por causa da avaliação preguiçosa.
Para quebrá-lo ainda mais para baixo:
Tome zip na entrada: '(1 2 3) A func foldr é chamado com
el->3, func->(lambda (a) empty)
Isso expande para:
(lambda (a) (cons (cons el (first a)) (func (rest a))))
(lambda (a) (cons (cons 3 (first a)) ((lambda (a) empty) (rest a))))
Se tivéssemos que voltar isto agora, nós teríamos uma função que leva uma lista de um elemento e retorna o par (3 elemento):
> (define f (lambda (a) (cons (cons 3 (first a)) ((lambda (a) empty) (rest a)))))
> (f (list 9))
(list (cons 3 9))
Continuada, foldr agora chama func com
el->3, func->f ;using f for shorthand
(lambda (a) (cons (cons el (first a)) (func (rest a))))
(lambda (a) (cons (cons 2 (first a)) (f (rest a))))
Esta é uma func que leva uma lista com dois elementos, agora, e fecha-los com (list 2 3)
:
> (define g (lambda (a) (cons (cons 2 (first a)) (f (rest a)))))
> (g (list 9 1))
(list (cons 2 9) (cons 3 1))
O que está acontecendo?
(lambda (a) (cons (cons 2 (first a)) (f (rest a))))
a
, neste caso, é (list 9 1)
(cons (cons 2 (first (list 9 1))) (f (rest (list 9 1))))
(cons (cons 2 9) (f (list 1)))
E, como você se lembra, f
fecha seu argumento com 3
.
E este continua etc ...
O problema com todas estas soluções para zip
é que eles só dobre uma lista ou de outra, o que pode ser um problema se ambos são "bons produtores", na linguagem de fusão lista. O que você realmente precisa é uma solução que se dobra em ambas as listas. Felizmente, há um artigo sobre exatamente isso, chamado "Coroutining Folds com Hyperfunctions" .
Você precisa de um tipo auxiliar, uma hiperfunção, que é basicamente uma função que toma outra hiperfunção como seu argumento.
newtype H a b = H { invoke :: H b a -> b }
Os hyperfunctions usadas aqui, basicamente, agir como uma "pilha" de funções comuns.
push :: (a -> b) -> H a b -> H a b
push f q = H $ \k -> f $ invoke k q
Você também precisa encontrar uma maneira de colocar dois hyperfunctions juntos, de ponta a ponta.
(.#.) :: H b c -> H a b -> H a c
f .#. g = H $ \k -> invoke f $ g .#. k
Isto está relacionado com push
pela lei:
(push f x) .#. (push g y) = push (f . g) (x .#. y)
Este acaba por ser um operador associativo, e esta é a identidade:
self :: H a a
self = H $ \k -> invoke k self
Você também precisa de algo que desconsidera tudo na "pilha" e retorna um valor específico:
base :: b -> H a b
base b = H $ const b
E, finalmente, você precisa de uma maneira de obter um valor fora de uma hiperfunção:
run :: H a a -> a
run q = invoke q self
run
cordas todas as funções push
ed juntos, ponta a ponta, até que ela atinge um base
ou loops infinitamente.
Então, agora você pode dobrar ambas as listas em hyperfunctions, usando funções que passam informações de um para o outro, e montar o valor final.
zip xs ys = run $ foldr (\x h -> push (first x) h) (base []) xs .#. foldr (\y h -> push (second y) h) (base Nothing) ys where
first _ Nothing = []
first x (Just (y, xys)) = (x, y):xys
second y xys = Just (y, xys)
A razão pela qual dobrando tanto em matéria listas é por causa de algo GHC faz chamado lista de fusão , que é falado em o GHC.Base módulo , mas provavelmente deve ser muito mais bem conhecido. Sendo um produtor boa lista e usando build
com foldr
pode evitar muita produção inútil e consumo imediato de elementos da lista, e pode expor ainda mais otimizações.
Eu tentei entender esta solução elegante mim mesmo, então eu tentei obter os tipos e avaliação de mim mesmo. Então, precisamos escrever uma função:
zip xs ys = foldr step done xs ys
Aqui precisamos step
derive e done
, sejam eles quais forem. Lembre foldr
's digita, instanciado para listas:
foldr :: (a -> state -> state) -> state -> [a] -> state
No entanto a nossa invocação foldr
deve ser instanciado para algo como abaixo, porque temos de aceitar não um, mas dois argumentos da lista:
foldr :: (a -> ? -> ?) -> ? -> [a] -> [b] -> [(a,b)]
Porque ->
é direito associativo , isso é equivalente a:
foldr :: (a -> ? -> ?) -> ? -> [a] -> ([b] -> [(a,b)])
Nossos corresponde ([b] -> [(a,b)])
para tipo state
variável na assinatura de tipo foldr
original, portanto, temos de substituir todas as ocorrências de state
com ele:
foldr :: (a -> ([b] -> [(a,b)]) -> ([b] -> [(a,b)]))
-> ([b] -> [(a,b)])
-> [a]
-> ([b] -> [(a,b)])
Isto significa que os argumentos que passamos para foldr
deve ter os seguintes tipos:
step :: a -> ([b] -> [(a,b)]) -> [b] -> [(a,b)]
done :: [b] -> [(a,b)]
xs :: [a]
ys :: [b]
Lembre-se que foldr (+) 0 [1,2,3]
expande para:
1 + (2 + (3 + 0))
Portanto, se xs = [1,2,3]
e ys = [4,5,6,7]
, nossa invocação foldr
se expandiria para:
1 `step` (2 `step` (3 `step` done)) $ [4,5,6,7]
Isto significa que nossa construção 1 `step` (2 `step` (3 `step` done))
deve criar uma função recursiva que iria passar por [4,5,6,7]
e fechar acima dos elementos. (Tenha em mente que, se uma das listas originais é mais longo, os valores em excesso são jogados fora). IOW, nossa construção deve ter o tipo [b] -> [(a,b)]
.
3 `step` done
é o nosso caso base, onde done
é um valor inicial, como 0
em foldr (+) 0 [1..3]
. Nós não queremos zip nada depois de 3, pois 3 é o valor final de xs
, por isso temos de terminar a recursão. Como você terminar a recursão sobre lista no caso base? Você retorna vazio lista []
. Mas tipo de recordação done
assinatura:
done :: [b] -> [(a,b)]
Portanto, não podemos retornar apenas []
, devemos retornar uma função que iria ignorar tudo o que recebe. Portanto, use const
:
done = const [] -- this is equivalent to done = \_ -> []
Agora vamos começar a descobrir o que step
deve ser. Ele combina um valor do tipo a
com uma função do tipo [b] -> [(a,b)]
e retorna uma função do tipo [b] -> [(a,b)]
.
Em 3 `step` done
, sabemos que o valor do resultado que mais tarde viria a nossa lista zipado deve ser (3,6)
(sabendo de xs
original e ys
). Portanto 3 `step` done
deve avaliar em:
\(y:ys) -> (3,y) : done ys
Lembre-se, temos de voltar uma função, dentro do qual temos alguma forma fechar acima dos elementos, o código acima é o que faz sentido e typechecks.
Agora que nós assumimos como exatamente step
deve avaliar, vamos continuar a avaliação. Veja como todas as medidas de redução do nosso foldr
olhar de avaliação como:
3 `step` done -- becomes
(\(y:ys) -> (3,y) : done ys)
2 `step` (\(y:ys) -> (3,y) : done ys) -- becomes
(\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys)
1 `step` (\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys) -- becomes
(\(y:ys) -> (1,y) : (\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys) ys)
A avaliação dá origem a esta implementação da etapa (note que representam ys
a esgotar-se de elementos cedo, retornando uma lista vazia):
step x f = \[] -> []
step x f = \(y:ys) -> (x,y) : f ys
Assim, o zip
função completa é implementada da seguinte forma:
zip :: [a] -> [b] -> [(a,b)]
zip xs ys = foldr step done xs ys
where done = const []
step x f [] = []
step x f (y:ys) = (x,y) : f ys
PS: Se você está inspirado pela elegância de dobras, leia Escrevendo foldl usando foldr e, em seguida, Graham Hutton do Um tutorial sobre a universalidade ea expressividade das vezes .
Uma abordagem simples:
lZip, rZip :: Foldable t => [b] -> t a -> [(a, b)]
-- implement zip using fold?
lZip xs ys = reverse.fst $ foldl f ([],xs) ys
where f (zs, (y:ys)) x = ((x,y):zs, ys)
-- Or;
rZip xs ys = fst $ foldr f ([],reverse xs) ys
where f x (zs, (y:ys)) = ((x,y):zs, ys)