haskell - Implementar zip usando foldr
functional-programming combinators (7)
Actualmente estoy en el capítulo 4 de Real World Haskell, y estoy tratando de entender qué es foldl en términos de foldr .
(Aquí está su 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)
Pensé que intentaría implementar zip
usando la misma técnica, pero parece que no estoy progresando. ¿Es posible?
Encontré una manera de usar un método bastante similar al tuyo:
myzip = foldr step (const []) :: [a] -> [b] -> [(a,b)]
where step a f (b:bs) = (a,b):(f bs)
step a f [] = []
Para los Haskellers no nativos aquí, he escrito una versión Scheme de este algoritmo para aclarar lo que está sucediendo realmente:
> (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))
El foldr
da foldr
resultado una función que, cuando se aplica a una lista, devuelve el zip de la lista plegada con la lista asignada a la función. El Haskell oculta el lambda
interno debido a la evaluación perezosa.
Para dividirlo más:
Tome la entrada zip on: ''(1 2 3) El func foldr se llama con
el->3, func->(lambda (a) empty)
Esto se expande a:
(lambda (a) (cons (cons el (first a)) (func (rest a))))
(lambda (a) (cons (cons 3 (first a)) ((lambda (a) empty) (rest a))))
Si tuviéramos que devolver esto ahora, tendríamos una función que toma una lista de un elemento y devuelve el par (3 elementos):
> (define f (lambda (a) (cons (cons 3 (first a)) ((lambda (a) empty) (rest a)))))
> (f (list 9))
(list (cons 3 9))
Continuando, foldr ahora llama func con
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 es una función que toma una lista con dos elementos, ahora, y los comprime con (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))
¿Qué esta pasando?
(lambda (a) (cons (cons 2 (first a)) (f (rest a))))
a
, en este caso, es (list 9 1)
(cons (cons 2 (first (list 9 1))) (f (rest (list 9 1))))
(cons (cons 2 9) (f (list 1)))
Y, como recordarás, f
zips su argumento con 3
.
Y esto continúa, etc ...
La respuesta ya se ha dado aquí, pero no una derivación (ilustrativa). Entonces, incluso después de todos estos años, quizás valga la pena agregarlo.
En realidad es bastante simple. Primero,
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) ...)))
por lo tanto, por eta-expansión,
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 es evidente aquí, si f
no está forzando en su segundo argumento , primero funciona en x1
e ys
, f x1
r1
ys
donde r1 =
(f x2 (f x3 (... (f xn z) ...)))
= foldr fz [x2,x3,...,xn]
.
Entonces, usando
f x1 r1 [] = [] f x1 r1 (y1:ys1) = (x1,y1) : r1 ys1
organizamos el paso de información de izquierda a derecha a lo largo de la lista, llamando a r1
con el resto de la lista de entrada ys1
, foldr fz [x2,x3,...,xn]
ys1 = f x2
ys1
r2
ys1
, como el siguiente paso. Y eso es eso.
Cuando ys
es más corto que xs
(o la misma longitud), el caso []
de ffres y el procesamiento se detiene. Pero si ys
es más largo que xs
entonces el caso de f
''s []
no se disparará y llegaremos a la aplicación f xn
z
(yn:ysn)
,
f xn z (yn:ysn) = (xn,yn) : z ysn
Como hemos llegado al final de xs
, el procesamiento zip
debe detenerse:
z _ = []
Y esto significa que se debe usar la definición z = const []
:
zip xs ys = foldr f (const []) xs ys
where
f x r [] = []
f x r (y:ys) = (x,y) : r ys
Desde el punto de vista de f
, r
desempeña el papel de una continuación de éxito , que llama cuando el procesamiento debe continuar, después de haber emitido el par (x,y)
.
Así que r
es "lo que se hace con más ys
cuando hay más x
s" , y z = const []
, el nil
-case en foldr
, es "lo que se hace con ys
cuando ya no hay x
s" . O f
puede detenerse por sí mismo, devolviendo []
cuando ys
está agotado.
Observe cómo ys
se usa como un tipo de valor acumulado, que se pasa de izquierda a derecha a lo largo de la lista xs
, de una invocación de f
a la siguiente (el paso "acumular" es, aquí, quitarle un elemento principal).
Naturalmente, esto corresponde al pliegue izquierdo, donde un paso de acumulación es "aplicar la función", con z = id
devuelve el valor acumulado final cuando "no hay más x
s":
foldl f a xs =~ foldr (/x r a-> r (f a x)) id xs a
Del mismo modo, para listas finitas,
foldr f a xs =~ foldl (/r x a-> r (f x a)) id xs a
Y dado que la función de combinación puede decidir si continuar o no, ahora es posible tener un doblez izquierdo que puede detenerse temprano:
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
o un salto hacia la izquierda pliegue, foldlWhen t ...
, con
cons x r a = if t x then r (f a x) else r a
etc.
El problema con todas estas soluciones para zip
es que solo se pliegan en una u otra lista, lo que puede ser un problema si ambos son "buenos productores", en el lenguaje de la lista de fusión. Lo que realmente necesita es una solución que se pliega en ambas listas. Afortunadamente, hay un artículo sobre exactamente eso, llamado "pliegues Coroutining con hiperfunciones" .
Necesita un tipo auxiliar, una hiperfunción, que es básicamente una función que toma otra hiperfunción como argumento.
newtype H a b = H { invoke :: H b a -> b }
Las hiperfunciones utilizadas aquí básicamente actúan como una "pila" de funciones ordinarias.
push :: (a -> b) -> H a b -> H a b
push f q = H $ /k -> f $ invoke k q
También necesita una forma de juntar dos hiperfunciones, de extremo a extremo.
(.#.) :: H b c -> H a b -> H a c
f .#. g = H $ /k -> invoke f $ g .#. k
Esto está relacionado con push
por la ley:
(push f x) .#. (push g y) = push (f . g) (x .#. y)
Esto resulta ser un operador asociativo, y esta es la identidad:
self :: H a a
self = H $ /k -> invoke k self
También necesita algo que ignora todo lo demás en la "pila" y devuelve un valor específico:
base :: b -> H a b
base b = H $ const b
Y finalmente, necesita una forma de obtener un valor de una hiperfunción:
run :: H a a -> a
run q = invoke q self
run
cadenas de todas las funciones push
ed juntas, de extremo a extremo, hasta que toque una base
o bucles infinitamente.
Entonces ahora puede doblar ambas listas en hiperfunciones, usando funciones que pasan información de una a la otra, y ensamblar el 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)
La razón por la que doblar ambas listas importa es por algo que GHC llama fusión de listas , de lo que se habla en el módulo GHC.Base , pero probablemente debería ser mucho más conocido. Ser un buen productor de listas y usar build
con foldr
puede evitar la producción inútil y el consumo inmediato de elementos de lista, y puede exponer más optimizaciones.
Traté de entender esta elegante solución yo mismo, así que traté de derivar los tipos y la evaluación de mí mismo. Entonces, necesitamos escribir una función:
zip xs ys = foldr step done xs ys
Aquí tenemos que derivar step
y done
, sean lo que sean. foldr
el tipo de foldr
, instanciado en listas:
foldr :: (a -> state -> state) -> state -> [a] -> state
Sin embargo, nuestra invocación de foldr
debe instanciar a algo como a continuación, porque no debemos aceptar uno, sino dos argumentos de lista:
foldr :: (a -> ? -> ?) -> ? -> [a] -> [b] -> [(a,b)]
Porque ->
tiene una asociación correcta , esto es equivalente a:
foldr :: (a -> ? -> ?) -> ? -> [a] -> ([b] -> [(a,b)])
Nuestro ([b] -> [(a,b)])
corresponde a state
variable de tipo de state
en la firma de tipo de foldr
original, por lo tanto, debemos reemplazar cada aparición de state
con ella:
foldr :: (a -> ([b] -> [(a,b)]) -> ([b] -> [(a,b)]))
-> ([b] -> [(a,b)])
-> [a]
-> ([b] -> [(a,b)])
Esto significa que los argumentos que pasamos a foldr
deben tener los siguientes tipos:
step :: a -> ([b] -> [(a,b)]) -> [b] -> [(a,b)]
done :: [b] -> [(a,b)]
xs :: [a]
ys :: [b]
Recuerde que foldr (+) 0 [1,2,3]
expande a:
1 + (2 + (3 + 0))
Por lo tanto, si xs = [1,2,3]
y ys = [4,5,6,7]
, nuestra invocación de foldr
se expandiría a:
1 `step` (2 `step` (3 `step` done)) $ [4,5,6,7]
Esto significa que nuestra 1 `step` (2 `step` (3 `step` done))
debe crear una función recursiva que pasará por [4,5,6,7]
y comprimir los elementos. (Tenga en cuenta que si una de las listas originales es más larga, los valores sobrantes se descartan). IOW, nuestra construcción debe tener el tipo [b] -> [(a,b)]
.
3 `step` done
es nuestro caso base, donde done
es un valor inicial, como 0
en foldr (+) 0 [1..3]
. No deseamos comprimir nada después de 3, porque 3 es el valor final de xs
, por lo que debemos finalizar la recursión. ¿Cómo terminas la recursión sobre la lista en el caso base? Usted devuelve la lista vacía []
. Pero recuerda la firma del tipo done
:
done :: [b] -> [(a,b)]
Por lo tanto, no podemos devolver solo []
, debemos devolver una función que ignore lo que reciba. Por lo tanto, use const
:
done = const [] -- this is equivalent to done = /_ -> []
Ahora comencemos a pensar qué step
debería ser. Combina un valor de tipo a
con una función de tipo [b] -> [(a,b)]
y devuelve una función de tipo [b] -> [(a,b)]
.
En 3 `step` done
, sabemos que el valor del resultado que luego iría a nuestra lista comprimida debe ser (3,6)
(sabiendo de xs
y ys
originales). Por lo tanto, 3 `step` done
debe evaluar en:
/(y:ys) -> (3,y) : done ys
Recuerde, debemos devolver una función, dentro de la cual, de alguna manera, ajustamos los elementos, el código anterior es lo que tiene sentido y las comprobaciones de tipo.
Ahora que asumimos cómo debería evaluarse exactamente el step
, continuemos la evaluación. Así es como se ven todos los pasos de reducción en nuestra evaluación de foldr
:
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)
La evaluación da lugar a esta implementación del paso (tenga en cuenta que damos cuenta de que y se está quedando sin elementos al principio al devolver una lista vacía):
step x f = /[] -> []
step x f = /(y:ys) -> (x,y) : f ys
Por lo tanto, la función completa zip
se implementa de la siguiente manera:
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
PD: Si te sientes inspirado por la elegancia de los pliegues, lee Writing foldl con foldr y luego el tutorial A de Graham Hutton sobre la universalidad y expresividad de fold .
Un enfoque simple:
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)
zip2 xs ys = foldr step done xs ys
where done ys = []
step x zipsfn [] = []
step x zipsfn (y:ys) = (x, y) : (zipsfn ys)
Cómo funciona esto: (foldr step done xs) devuelve una función que consume ys; de modo que bajamos por la lista xs construyendo una composición anidada de funciones que se aplicarán a la parte correspondiente de ys.
Cómo idearlo: comencé con la idea general (de ejemplos similares vistos anteriormente), escribí
zip2 xs ys = foldr step done xs ys
luego llenó cada una de las siguientes líneas con lo que tenía que ser para hacer que los tipos y valores salieran bien. Era más fácil considerar los casos más simples antes que los más difíciles.
La primera línea podría escribirse más simplemente como
zip2 = foldr step done
como Mattiast mostró.