Laboratorio Análisis Lógico Práctica 4: Funciones anónimas, E/S básica Pedro Arturo Góngora Luna1 1. Funciones anónimas A veces puede ser útil utilizar una función sin tener que darle un nombre. Esto se puede lograr usando una expresión lambda. Una expresión lambda representa a una función, sólo que ésta carece de un nombre. Una expresión lambda tiene la siguiente sintaxis (simplificada): λx1 . . . xn . exp donde x1 . . . xn son los parámetros de la función y exp es cualquier expresión que contenga a dichos argumentos. Para evaluar el resultado de aplicar una función anónima a un argumento dado, simplemente sustituimos cada ocurrencia de los parámetros por el valor proporcionado. El ejemplo más sencillo es la función identidad λx. x aplicada a 1 (λx. x) 1 = 1 La función sucesor puede definirse como λx. x + 1 de manera que (λx. x + 1) 10 = 10 + 1 = 11 Podemos escribir una función que sume dos argumentos dados λx y. x + y Para calcular el resultado de aplicar la función anterior a dos números, sustituimos, paso a paso, las ocurrencias de x, y en x + y por los números proporcionados. Por ejemplo (λx y. x + y) 4 2 = (λy. 4 + y) 2 = 4+2 = 6 En Haskell, la sintaxis de una expresión lambda es casi la misma. En lugar de los sı́mbolos λ y . usamos los caracteres \ y ->, respectivamente. Podemos probar directamente en el intérprete las funciones anteriores: 1 [email protected] 1 Main> (\x -> x) 1 1 Main> (\x -> x+1) 10 11 Main> (\x y -> x+y) 4 2 6 2. 2.1. E/S El problema de la E/S Haskell es un lenguaje funcional “puro”, por lo que cuenta con una propiedad llamada integridad referencial. Como consecuencia, Haskell no permite efectos secundarios (no existe la asignación destructiva) y permite un razonamiento de tipo ecuacional. La integridad referencial nos dice que el significado de una expresión está determinado únicamente por el significado de sus subexpresiones. Esta propiedad se logra ya que las funciones en Haskell son realmente funciones, a diferencia de los lenguajes imperativos, donde en realidad son subprogramas. Por ejemplo, una expresión como x − x, siempre será evaluada a 0, independientemente de cómo se definió x. Para entender el problema con la E/S, supongamos que tenemos una función getInt que devuelve un entero proporcionado por el usuario. El resultado de la expresión getInt - getInt no necesariamente evalúa a 0, pues depende de los números proporcionados por el usuario. Una expresión de este tipo rompe con la integridad referencial, pues en realidad no es una función (i.e., no devuelve siempre un mismo valor para una misma entrada). Los primeros lenguajes funcionales solucionaron este problema simplemente permitiendo efectos secundarios para algunas de sus expresiones. Los lenguajes como Haskell adoptan una solución basada en mónadas, pertenecientes a un área de las matemáticas llamada Teorı́a de las Categorı́as. 2.2. Acciones secuenciales Aunque la teorı́a detrás de las mónadas puede ser complicada, su implementación es muy sencilla. En Haskell, una mónada es un tipo de dato algebraico (similar a los vistos en la práctica anterior) cuyo propósito es “envolver” algún dato para realizar operaciones que requieran efectos secundarios con él, pero sin afectar el paradigma funcional. En el preludio de Haskell, están definidas dos funciones getLine y putStrLn. La primera lee una cadena proporcionada por el usuario, mientras que la segunda imprime una cadena en la consola. Si nos proponemos el objetivo de escribir un programa que lea una lı́nea de texto de la consola y después simplemente la imprima de nuevo, podemos pensar en un algorimo sencillo como el siguiente: 1. Leer entrada del teclado y guardarlo en una variable 2. Imprimir el texto guardado en la pantalla Nuestro algoritmo cuenta con dos pasos, o mejor dicho, realiza dos acciones secuenciales. Si getLine y putStrLn son esas dos acciones, lo único que nos hace falta es una manera de componerlas secuencialmente. En una práctica previa, se mencionó la existencia del operador >>=, cuyo propósito es, precisamente, la composición secuencial de acciones. De esta forma sólo queda un problema por resolver, el algortimo pide una asignación a variable. 2 Veamos la solución primero para posteriormente explicarla, escribimos en un archivo Prueba.hs la función echo: import IO echo = getLine >>= putStrLn De forma que, al evaluar echo en el intérprete resulta lo siguiente: Main> echo hola hola Main> Para comprender el funcionamiento del programa, tenemos que examinar el tipo de las funciones getLine y putStrLn, ası́ como, el tipo del operador >>= Prelude> :t getLine getLine :: IO String Prelude> :t putStr putStr :: String -> IO () Main> :t (>>=) (>>=) :: Monad a => a b -> (b -> a c) -> a c Lo que nos dice los tipos es lo siguiente getLine es una expresión de tipo IO String, esto es una mónada IO que envuelve a un dato de tipo String. putStrLn es una función que recibe una cadena y devuelve un dato IO (), donde () es un elemento que no tiene valor, pero que usamos por que nos interesan los efectos secundarios de ejecutar la función (() es similar al tipo void de algunos lenguajes imperativos). El operador >>= desenvuelve el dato que regresa la primera función y se lo envı́a como parámetro a la segunda. Ahora ¿qué pasarı́a si necesitáramos “manipular” el valor devuelto por getLine? Para ese propósito, podemos usar una función anónima: import IO echo = getLine >>= (\x -> putStrLn ("Escribiste: " ++ x)) >> putStrLn "bye" 3 Donde >> funciona como >>=, sólo que descarta el dato devuelto por la primera función (en este caso ()). El resultado de esta nueva función serı́a: Main> echo Me gusta Haskell Escribiste: Me gusta Haskell bye 2.3. Notación do El uso de los operadores >>= y >> es sencillo, sin embargo, Haskell provee una notación abreviada similar al la que usarı́amos en un lenguaje imperativo. Por ejemplo, el programa echo anterior, puede escribirse también como import IO echo = do x <- getLine putStrLn ("Escribiste: " ++ x) putStrLn "bye" En esta notación, podemos pensar en el operador <- como la asignación de un lenguaje imperativo. Esta analogı́a no es del todo real, pues la notación do sólo es una abreviatura del uso de >>=, >> y funciones anónimas. Sin embargo, esta notación provee de una forma más legible para escribir acciones secuenciales. Otro ejemplo: echo_repeat = do x <- getLine if x == "bye" then return () else do putStrLn ("Escribiste: " ++ x) echo_repeat Como nota final, es conveniente resaltar que las acciones del programa anterior están alineadas (indentadas), una debajo de la otra. En Haskell la indentación es importante, pues sirve para delimitar el alcance de algunos constructores23 . En este caso, las tres acciones pertenecen al mismo do. 3. Ejercicios 1. Escribe un programa que lea una lı́nea de texto de la consola y decida si es un palı́ndromo o no, y después imprima la respuesta. 2. Escribe un programa similar al anterior, sólo que ahora el programa debe continuar leyendo lı́neas de la entrada hasta que el usuario introduzca una lı́nea vacı́a (i.e., sólo presione la tecla Enter sin escribir nada). TIP: Puedes consultar el capı́tulo 18 del siguiente libro para ayudarte con el segundo ejercicio: Thompson, Simon, Haskell: The Craft of Functional Programming 2nd. Ed., publicado por Addison Wesley 2 También 3 Otro se pueden usar los sı́mbolos ;, { y } como en Java o C, pero se prefiere éste método. lenguaje que usa un sistema de indentación similar es Python 4