Laboratorio Análisis Lógico

Anuncio
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
Descargar