Programación Funcional Alberto Pardo Marcos Viera Instituto de Computación, Facultad de Ingenierı́a Universidad de la República, Uruguay Alberto Pardo, Marcos Viera Programación Funcional Lazy evaluation Alberto Pardo, Marcos Viera Programación Funcional Estrategias de evaluación Una expresión formada por una función aplicada a argumentos que está en condiciones de ser reducida es llamada un redex (por reducible expression). Existen dos estrategias que son comunmente usadas para evaluar expresiones. Dichas estrategias difieren en el redex que escogen para reducir en cada paso. Consideremos la siguiente expresión: square (1 + 2) where square x = x ∗ x y veamos en que orden la podemos evaluar. Alberto Pardo, Marcos Viera Programación Funcional Estrategia: leftmost innermost Se elije el redex más interno (el que no contiene otros redexes) más a la izquierda. square (1 + 2) { def. + } = square 3 { def. square } = 3∗3 { def. + } = 9 Alberto Pardo, Marcos Viera Programación Funcional Estrategia: leftmost innermost Se elije el redex más interno (el que no contiene otros redexes) más a la izquierda. square (1 + 2) { def. + } = square 3 { def. square } = 3∗3 { def. + } = 9 Corresponde a pasaje por valor (call-by-value). Alberto Pardo, Marcos Viera Programación Funcional Estrategia: leftmost outermost Se elije el redex más externo (no contenido por otros redexes) más a la izquierda. square (1 + 2) = { def. square } (1 + 2) ∗ (1 + 2) = { def. + } 3 ∗ (1 + 2) = { def. + } 3∗3 = { def. * } 9 Alberto Pardo, Marcos Viera Programación Funcional Estrategia: leftmost outermost Se elije el redex más externo (no contenido por otros redexes) más a la izquierda. square (1 + 2) = { def. square } (1 + 2) ∗ (1 + 2) = { def. + } 3 ∗ (1 + 2) = { def. + } 3∗3 = { def. * } 9 Corresponde a pasaje por nombre (call-by-name). Alberto Pardo, Marcos Viera Programación Funcional Terminación La evaluación de una expresión puede parar (o terminar) retornando un valor, o seguir por siempre (no terminación). Alberto Pardo, Marcos Viera Programación Funcional Terminación La evaluación de una expresión puede parar (o terminar) retornando un valor, o seguir por siempre (no terminación). Propiedad 1: Si dos secuencias de reducción distintas de una misma expresión terminan, entonces lo van a hacer con el mismo resultado. Alberto Pardo, Marcos Viera Programación Funcional Terminación La evaluación de una expresión puede parar (o terminar) retornando un valor, o seguir por siempre (no terminación). Propiedad 1: Si dos secuencias de reducción distintas de una misma expresión terminan, entonces lo van a hacer con el mismo resultado. Propiedad 2: Dada una expresión, si existe alguna secuencia de evaluación que termina con un cierto resultado, entonces la estrategia call-by-name también terminará produciendo el mismo resultado final. En cambio, la estrategia call-by-value no garantiza llegar a ese resultado. Alberto Pardo, Marcos Viera Programación Funcional No terminación Sea la siguiente definición: inf :: Int inf = 1 + inf La evaluación de inf no termina independiente de la estrategia de evaluación. Produce expresiones cada vez más grandes. Decimos que su valor es indefinido. De todas formas su ecuación se usa como regla de reescritura. Alberto Pardo, Marcos Viera Programación Funcional Terminación: call-by-value fst (length [ ], inf ) = fst (0, inf ) = fst (0, 1 + inf ) = fst (0, 1 + (1 + inf )) = fst (0, 1 + (1 + (1 + inf ))) = ... La evaluación no termina. Alberto Pardo, Marcos Viera Programación Funcional Terminación: call-by-name fst (length [ ], inf ) = length [ ] = 0 El redex más externo es la propia expresión (fst aplicado al par). Esto muestra que un argumento es evaluado sólo si es necesario (por demanda). Alberto Pardo, Marcos Viera Programación Funcional Terminación: call-by-name three :: Int → Int three x = 3 three inf = 3 Alberto Pardo, Marcos Viera Programación Funcional Terminación: call-by-name length [inf , 2] = 1 + length [2] = 1 + (1 + length [ ]) = 1 + (1 + 0) = 2 Alberto Pardo, Marcos Viera Programación Funcional Funciones estrictas Al valor indefinido se lo suele denotar con el sı́mbolo especial ⊥ que se pronuncia “bottom”. Denota tanto el caso de no terminación como el de detención anormal de un programa. Un valor ⊥ existe en todos los tipos. Por ejemplo, el valor de inf es el valor ⊥ del tipo Int, mientras que el valor de 1/0 es el valor indefinido de tipo Float. Se dice que una función es estricta si f ⊥ = ⊥. En otro caso se dice que es no estricta. Ejemplos: +, *, -, /, div , mod son todas funciones estrictas. En cambio, three es no estricta. Alberto Pardo, Marcos Viera Programación Funcional Lazy evaluation Retomemos la evaluación de square (1 + 2) con call-by-name. square (1 + 2) = (1 + 2) ∗ (1 + 2) = 3 ∗ (1 + 2) = 3∗3 = 9 Problema: la subexpresión 1 + 2 es evaluada dos veces. Propiedad: con call-by-value los argumentos son evaluados una sola vez, en cambio con call-by-name pueden llegar a ser evaluados muchas veces. Alberto Pardo, Marcos Viera Programación Funcional Lazy evaluation (2) El problema se resuelve usando punteros para indicar que hay subexpresiones que se comparten durante la evaluación. Las expresiones se escriben entonces como grafos en lugar de como árboles. En lugar de copiar fı́sicamente el argumento en todos los lugares del cuerpo de la función donde se usa, simplemente tenemos una copia del argumento y colocamos varios punteros a él desde las posiciones del cuerpo. De esta manera, cualquier reducción que se haga sobre el argumento va a ser compartida por todas las posiciones que tienen punteros al argumento. Por lo tanto, la evaluación de un argumento duplicado se hace a lo más una vez. El uso de call-by-name junto con sharing es llamado lazy evaluation o call-by-need. Alberto Pardo, Marcos Viera Programación Funcional Ejemplo de lazy evaluation Escribimos variables locales para denotar punteros. square (1 + 2) = a∗a where a = 1 + 2 = a∗a where a = 3 = 9 Alberto Pardo, Marcos Viera Programación Funcional Funciones no estrictas True && b = b False && b = False True || b = True False || b = b if True then e else = e if False then else e = e if ⊥ then else = ⊥ El if es estricto en la condición, pero no en las ramas. Alberto Pardo, Marcos Viera Programación Funcional Explotando lazy evaluation Igualdad de listas. [] == [ ] = True [] == ( : ) = False = False ( : ) == [ ] (x : xs) == (y : ys) = (x == y ) && (xs == ys) Ejemplos: [1, 2] == [1, 2] True [1, 2, inf ] == [1, 2] Alberto Pardo, Marcos Viera Programación Funcional Explotando lazy evaluation Igualdad de listas. [] == [ ] = True [] == ( : ) = False = False ( : ) == [ ] (x : xs) == (y : ys) = (x == y ) && (xs == ys) Ejemplos: [1, 2] == [1, 2] True [1, 2, inf ] == [1, 2] False [1, inf , 2] == [1, 2, 3] Alberto Pardo, Marcos Viera Programación Funcional Explotando lazy evaluation Igualdad de listas. [] == [ ] = True [] == ( : ) = False = False ( : ) == [ ] (x : xs) == (y : ys) = (x == y ) && (xs == ys) Ejemplos: [1, 2] == [1, 2] True [1, 2, inf ] == [1, 2] [1, inf , 2] == [1, 2, 3] False ⊥ Alberto Pardo, Marcos Viera Programación Funcional Explotando lazy evaluation Determinar si dos árboles contienen los mismos valores en las hojas y en el mismo orden. Los árboles pueden tener formas distintas. data Btree a = Leaf a | Fork (Btree a) (Btree a) eqleaves :: Ord a ⇒ Btree a → Btree a → Bool eqleaves t t 0 = leaves t == leaves t 0 leaves (Leaf a) = [a] leaves (Fork l r ) = leaves l + + leaves r Alberto Pardo, Marcos Viera Programación Funcional Explotando lazy evaluation Determinar si dos árboles contienen los mismos valores en las hojas y en el mismo orden. Los árboles pueden tener formas distintas. data Btree a = Leaf a | Fork (Btree a) (Btree a) eqleaves :: Ord a ⇒ Btree a → Btree a → Bool eqleaves t t 0 = leaves t == leaves t 0 leaves (Leaf a) = [a] leaves (Fork l r ) = leaves l + + leaves r Las listas leaves t y leaves t 0 se van construyendo a demanda. Alberto Pardo, Marcos Viera Programación Funcional Ejemplo de evaluación eqleaves (Fork (Leaf 2) (Leaf 3)) (Leaf 2) ⇒ leaves (Fork (Leaf 2) (Leaf 3)) == leaves (Leaf 2) ⇒ leaves (Leaf 2) + + leaves (Leaf 3) == leaves (Leaf 2) ⇒ [2] ++ leaves (Leaf 3) == leaves (Leaf 2) ⇒ 2 : ([ ] ++ leaves (Leaf 3)) == leaves (Leaf 2) ⇒ 2 : ([ ] ++ leaves (Leaf 3)) == 2 : [ ] ⇒ (2 == 2) && ([ ] + + leaves (Leaf 3) == [ ]) ⇒ True && ([ ] + + leaves (Leaf 3) == [ ]) ⇒ [ ] ++ leaves (Leaf 3) == [ ] ⇒ leaves (Leaf 3) == [ ] ⇒ 3 : [ ] == [ ] ⇒ False Alberto Pardo, Marcos Viera Programación Funcional Definición de eqleaves en un lenguaje no lazy eqleaves t t 0 = comparestacks [t ] [t 0 ] comparestacks comparestacks comparestacks comparestacks comparestacks comparestacks [] [] = True [ ] (t : ts) = False (t : ts) [ ] = False (Fork l r : ts) ts 0 = comparestacks (l : r : ts) ts 0 ts (Fork l r : ts 0 ) = comparestacks ts (l : r : ts 0 ) (Leaf a : ts) (Leaf b : ts 0 ) | a == b = comparestacks ts ts 0 | otherwise = False Alberto Pardo, Marcos Viera Programación Funcional Explotando lazy evaluation Determinar si existe algún elemento de una lista que cumple un predicado. exists :: (a → Bool) → [a] → Bool exists p [ ] = False exists p (x : xs) | p a = True | otherwise = exists p xs exists xs = not ◦ null ◦ filter p El procesamiento termina en cuanto un elemento pase el filter . En Haskell esta función es llamada any . Alberto Pardo, Marcos Viera Programación Funcional Estructuras infinitas Lazy evaluation permite algo que en principio parece imposible: programar con estructuras infinitas. Un ejemplo de esto lo vimos cuando evaluamos fst (length [ ], inf ) Gracias a lazy evaluation, evitamos producir la expresión infinita 1 + (1 + (1 + ...)) denotada por inf . Alberto Pardo, Marcos Viera Programación Funcional Listas infinitas Un ejemplo clásico de lista infinita es la lista de enteros ones: ones = 1 : ones Alberto Pardo, Marcos Viera Programación Funcional Listas infinitas Un ejemplo clásico de lista infinita es la lista de enteros ones: ones = 1 : ones Evaluar ones produce una lista infinita de unos. > ones [1, 1, 1, 1, 1, 1, 1, 1, ... Alberto Pardo, Marcos Viera Programación Funcional Listas infinitas Un ejemplo clásico de lista infinita es la lista de enteros ones: ones = 1 : ones Evaluar ones produce una lista infinita de unos. > ones [1, 1, 1, 1, 1, 1, 1, 1, ... Pero gracias a lazy evaluation podemos procesar sólo lo necesario de esta lista: head (tail ones) ⇒ head (tail (1 : ones)) ⇒ head ones ⇒ head (1 : ones) ⇒1 Alberto Pardo, Marcos Viera Programación Funcional Listas infinitas Lazy evaluation nos provee la siguente propiedad: de una expresión se evalúa únicamente lo que sea requerido por el contexto en la cual se usa. Bajo este concepto, más que como una lista infinita, ones puede verse como una lista potencialmente infinita. Esta idea no está restringida a listas, se aplica a cualquier estructura de datos. Alberto Pardo, Marcos Viera Programación Funcional Programación modular Lazy evaluation permite separar control de datos. Por ejemplo, un lista de tres 1s puede ser generada tomando los tres primeros elementos (control) de la lista ones (datos). > take 3 ones [1, 1, 1] Alberto Pardo, Marcos Viera Programación Funcional Programación modular Lazy evaluation permite separar control de datos. Por ejemplo, un lista de tres 1s puede ser generada tomando los tres primeros elementos (control) de la lista ones (datos). > take 3 ones [1, 1, 1] Algún cuidado es igualmente necesario para evitar no terminación: > filter (6 5) [1 . .] [1, 2, 3, 4, 5 Alberto Pardo, Marcos Viera Programación Funcional Programación modular Lazy evaluation permite separar control de datos. Por ejemplo, un lista de tres 1s puede ser generada tomando los tres primeros elementos (control) de la lista ones (datos). > take 3 ones [1, 1, 1] Algún cuidado es igualmente necesario para evitar no terminación: > filter (6 5) [1 . .] [1, 2, 3, 4, 5 En este caso es mejor algo de este estilo, ya que termina: > takeWhile (6 5) [1 . .] [1, 2, 3, 4, 5] Alberto Pardo, Marcos Viera Programación Funcional Funciones sobre listas infinitas iterate f x = [power f i x | i ← [0 . .]] power f 0 = id power f n = f ◦ power f (n − 1) iterate :: (a → a) → a → [a] iterate f x = x : iterate f (f x) [n . .] = iterate (+1) n nats = iterate (+1) 0 [m . . n ] = takeWhile (6 n) (iterate (+1) m) pot2 = iterate (∗2) 1 Alberto Pardo, Marcos Viera Programación Funcional Funciones sobre listas infinitas Dı́gitos de un número. digits :: Int → [Int ] digits = reverse ◦ map (‘mod‘10) ◦ takeWhile (6≡ 0) ◦ iterate (‘div ‘10) Alberto Pardo, Marcos Viera Programación Funcional Funciones sobre listas infinitas Dı́gitos de un número. digits :: Int → [Int ] digits = reverse ◦ map (‘mod‘10) ◦ takeWhile (6≡ 0) ◦ iterate (‘div ‘10) Quebrar una lista en segmentos de largo n. group :: Int → [[a]] group n = map (take n) ◦ takeWhile (not ◦ null) ◦ iterate (drop n) Alberto Pardo, Marcos Viera Programación Funcional Funciones sobre listas infinitas Dı́gitos de un número. digits :: Int → [Int ] digits = reverse ◦ map (‘mod‘10) ◦ takeWhile (6≡ 0) ◦ iterate (‘div ‘10) Quebrar una lista en segmentos de largo n. group :: Int → [[a]] group n = map (take n) ◦ takeWhile (not ◦ null) ◦ iterate (drop n) Esquema de generación de listas infinitas (streams): unfold :: (a → b) → (a → Bool) → (a → a) → a → [b ] unfold h p t = map h ◦ takeWhile p ◦ iterate t Alberto Pardo, Marcos Viera Programación Funcional La Criba de Eratóstenes La criba de Eratóstenes es un algoritmo que permite hallar todos los números primos. La descripción del algoritmo es esencialmente la siguiente: 1 Liste todos los números naturales desde el 2. 2 Marque el primer elemento p de esta lista como primo. 3 Tache todos los elementos múltiplos de p de la lista. 4 Retorne al paso 2. Una solución: primos = criba [2 . .] criba (p : xs) = p : criba [x | x ← xs, x ‘mod‘ p 6≡ 0] Alberto Pardo, Marcos Viera Programación Funcional Otras listas infinitas Definición alternativa de la lista de naturales: nats 0 = 0 : zipWith (+) ones nats 0 Lista de factoriales: factorials = 1 : zipWith (∗) [1 . .] factorials Otra forma menos eficiente: factorials 0 = map fact nats fact 0 = 1 fact n = n ∗ fact (n − 1) Alberto Pardo, Marcos Viera Programación Funcional Árboles infinitos btree1 = Fork (Leaf 5) btree1 btree2 = Fork (Fork btree2 (Leaf 2)) (Fork (Leaf 3) btree1 ) leftmost (Leaf x) = x leftmost (Fork l r ) = leftmost l Alberto Pardo, Marcos Viera Programación Funcional Árboles infinitos btree1 = Fork (Leaf 5) btree1 btree2 = Fork (Fork btree2 (Leaf 2)) (Fork (Leaf 3) btree1 ) leftmost (Leaf x) = x leftmost (Fork l r ) = leftmost l ejemplo1 = leftmost btree1 Alberto Pardo, Marcos Viera Programación Funcional Árboles infinitos btree1 = Fork (Leaf 5) btree1 btree2 = Fork (Fork btree2 (Leaf 2)) (Fork (Leaf 3) btree1 ) leftmost (Leaf x) = x leftmost (Fork l r ) = leftmost l ejemplo1 = leftmost btree1 ejemplo2 = leftmost btree2 Alberto Pardo, Marcos Viera Programación Funcional Árboles infinitos btree1 = Fork (Leaf 5) btree1 btree2 = Fork (Fork btree2 (Leaf 2)) (Fork (Leaf 3) btree1 ) leftmost (Leaf x) = x leftmost (Fork l r ) = leftmost l ejemplo1 = leftmost btree1 ejemplo2 = leftmost btree2 ejemplo3 = take 5 $ map (∗2) $ leaves btree1 Alberto Pardo, Marcos Viera Programación Funcional Árboles infinitos btree1 = Fork (Leaf 5) btree1 btree2 = Fork (Fork btree2 (Leaf 2)) (Fork (Leaf 3) btree1 ) leftmost (Leaf x) = x leftmost (Fork l r ) = leftmost l ejemplo1 = leftmost btree1 ejemplo2 = leftmost btree2 ejemplo3 = take 5 $ map (∗2) $ leaves btree1 ejemplo4 = take 5 $ map (∗2) $ leaves btree2 Alberto Pardo, Marcos Viera Programación Funcional