Hugs es un intérprete del lenguaje funcional puro Haskell.
Durante este taller escribiremos, probaremos y utilizaremos programas funcionales.
A Hugs se lo invoca desde la línea de comandos o picando sobre el icono en nuestro entorno gráfico. Una vez que el intérprete está activo, la pantalla se presenta con un prompt a la espera de expresiones a ser evaluadas o comandos.
[laura@azul Taller]$ hugs __ __ __ __ ____ ___ _________________________________________ || || || || || || ||__ Hugs 98: Based on the Haskell 98 standard ||___|| ||__|| ||__|| __|| Copyright (c) 1994-2005 ||---|| ___|| World Wide Web: http://haskell.org/hugs || || Report bugs to: hugs-bugs@haskell.org || || Version: March 2005 _________________________________________ Haskell 98 mode: Restart with command line option -98 to enable extensions Type :? for help Hugs.Base>
De este modo Hugs se convierte en una calculadora, esperando que se introduzca una expresión para evaluarla e imprimir el resultado (lazo leer-evaluar-imprimir). Después, nuestra calculadora Hugs queda lista para volver a pedir una expresión.
Gracias al preludio estándar tenemos muchas funciones ya definidas, entre ellas, todas las aritméticas y las lógicas básicas. Pero también tenemos otras muchas, como por ejemplo “reverse”.
Hugs.Base> 21+21 42 Hugs.Base> True && False False Hugs.Base> 2*4 == 10 `div` 5 && True False Hugs.Base> [2,3,5,7,11]++[1,2] [2,3,5,7,11,1,2] Hugs.Base> reverse "dabale arroz a la zorra el abad" "daba le arroz al a zorra elabad"
Podemos salir del intérprete Hugs con CTRL-D
o con el comando “:q”.
Hugs.Base> :q [Leaving Hugs] [laura@azul Taller]$
Así volvemos al modo normal de la computadora.
Para poder dar nuevas definiciones y/o funciones, además de las que se encuentran en el preludio estándar, necesitamos escribir un programa funcional o script Haskell.
Un programa funcional es un archivo con terminación .hs
donde se escriben en texto plano todas las definiciones que conforman el programa funcional.
Como ejemplo, vamos a ver todo el proceso de creación-carga-prueba-modificación-recarga, con el Ejercicio 8.3 del apunte Extracto del Cálculo de Programas:
sgn :: Int → Int -- dado un entero x, //sgn// retorna su signo, de la siguiente forma: -- retornará 1 si x es positivo, -1 si es negativo y 0 en cualquier otro caso.
Para crear un script basta con invocar el comando para editar un (nuevo) archivo :e cap8.hs
. También se puede crear el script mediante un editor de texto cualquiera (emacs, bloc de notas, kate, vi,…).
Hugs.Base> :e cap8.hs
Una posible solución para el problema de la función signo es la siguiente:
sgn :: Int -> Int sgn x | 0<x = 1 | x<0 = -1 | x=0 = 0
Luego de guardar el programa, hay cargarlo para que el intérprete Hugs pueda empezar a usar la nueva función que hemos creado.
Hugs.Base> :l cap8.hs ERROR "cap8.hs":4 - Syntax error in input (unexpected `=')
Pero el intérprete indica un error! Por qué? Porque “=” es el símbolo de definición, mientras que la comparación es “==”.
Las traducciones de los símbolos son más o menos directas, de todas formas preparamos una tabla de Traducción de "Cálculo de Programas" a Haskell.
Para solucionar el error, volvemos a editar el script con :e
(el nombre ya no lo necesitamos ya que tenemos cargado este script) y corregimos x=0
por x==0
.
sgn :: Int -> Int sgn x | 0<x = 1 | x<0 = -1 | x==0 = 0
Luego de guardar el programa, hay que volver a cargarlo para que el intérprete pueda usar la función corregida, si no lo cargamos, el intérprete se queda en el estado anterior, en el que no tenía la función porque ésta tenía un error.
Hugs.Base> :l cap8.hs Main>
Ahora sí, la función es correcta y por ello el intérprete nos muestra el prompt Main
, que indica que hemos cargado alguna función más de las que hay en el preludio estándar, y que las funciones que hemos cargado son correctas.
Podemos probar la nueva función con casos de test para ganar confianza en su corrección.
Main> sgn 1 1 Main> sgn 0 0 Main> sgn -1 ERROR - Cannot infer instance *** Instance : Num (Int -> Int) *** Expression : sgn - 1 Main> sgn (-1) -1 Main> sgn 123123123123 Program error: arithmetic overflow Main> sgn 123123123 1 Main> sgn 1.1 ERROR - Cannot infer instance *** Instance : Fractional Int *** Expression : sgn 1.1 Main> sgn (-1 ERROR - Syntax error in expression (unexpected end of input) Main> sgn "hola" ERROR - Type error in application *** Expression : sgn "hola" *** Term : "hola" *** Type : String *** Does not match : Int Main> map sgn [-10..10] [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,1,1,1,1,1,1,1,1,1,1]
Notamos los distintos tipos de errores que se producen por problemas de
sgn -1
)sgn 1.1
)sgn (-1
)Haskell es un lenguaje tipado, es decir, todos los datos pertenecen a una clase o tipo de datos. Hugs tiene una maquinaria para inferir tipos, tanto los declarados
Main> :t sgn sgn :: Int -> Int
Como expresiones en general
Main> :t 1 + 2.1 1 + 2.1 :: Fractional a => a Main> :t 1+ sqrt 64 1 + sqrt 64 :: Floating a => a Main> :t 1+ "a" ERROR - Cannot infer instance *** Instance : Num [Char] *** Expression : 1 + "a" Main> :t "hola" ++ "que" ++ "tal" "hola" ++ "que" ++ "tal" :: [Char] Main> :t reverse reverse :: [a] -> [a] Main> :t map map :: (a -> b) -> [a] -> [b]
Esta maquinaria impide que escribamos cualquier expresión que esté mal tipada.
Todas las funciones suelen ir encabezadas por su signatura, es decir, el nombre de la función junto al tipo de sus parámetros y resultado. El nombre va seguido de ::
y los parámetros y el resultado van separados por →
. Por ejemplo:
sgn :: Int -> Int reverse :: [a] -> [a] map :: (a -> b) -> [a] -> [b]
Haskell maneja n-uplas de manera directa. Las n-uplas nos permiten agrupar datos para manejarlos como una sola cosa, como veremos más adelante.
Incorporamos a cap8.hs
las siguientes definiciones:
suma3upla :: (Int,Int,Int) -> Int suma3upla (x,y,z) = x+y+z sumaYResta :: Int -> Int -> (Int,Int) sumaYResta x y = (x+y, x-y)
Y las probamos desde el prompt.
Main> suma3upla (2,3,4) 9 Main> sumaYResta 2 3 (5,-1)
Haskell también maneja listas de manera directa, pero lo veremos más adelante.
A manera de ejemplo veamos el ejercicio 8.7 del apunte, donde tenemos que definir una función muy útil para cualquier aparato que maneje un calendario (relojes, celulares, PDAs, computadoras, DVD-R, etc.). La signatura es bisiesto: Int → Bool, y es un predicado que devuelve true si el año es bisiesto y false en caso contrario. Recordemos cuando un año es bisiesto:
La regla para los años bisiestos según el calendario gregoriano es: Un año es bisiesto si es divisible por 4, excepto los principios de siglo (aquellos divisibles por 100), que para ser bisiestos, también deben ser divisibles por 400.
Una definición matemática concisa sería bisiesto n = 4|n /\ (100|n ⇒ 400|n).
Entonces podemos seguir agregando definiciones de funciones a nuestro archivo cap8.hs
con el comando :e
.
Veamos tres versiones distintas 1).
La del viejo programadora/or
bisiesto'' :: Int -> Bool bisiesto'' n = if n `mod` 4 /= 0 then False else if n `mod` 100 /= 0 then True else if n `mod` 400 == 0 then True else False
El que está aprendiendo Haskell
bisiesto' :: Int -> Bool bisiesto' n | n `mod` 4 /= 0 = False | n `mod` 4 == 0 = n `mod` 100 /= 0 || n `mod` 400 == 0
El que cursó Introducción a los Algoritmos
bisiesto :: Int -> Bool bisiesto n = n `mod` 4 == 0 && (n `mod` 100 /= 0 || n `mod` 400 == 0)
Podemos poner las tres versiones en nuestro script y probarlas rápidamente usando la función filter
para que obtener solo los años bisiestos desde 1945 al 2006.
Main> filter bisiesto [1945..2006] [1948,1952,1956,1960,1964,1968,1972,1976,1980,1984,1988,1992,1996,2000,2004] Main> filter bisiesto' [1945..2006] [1948,1952,1956,1960,1964,1968,1972,1976,1980,1984,1988,1992,1996,2000,2004] Main> filter bisiesto'' [1945..2006] [1948,1952,1956,1960,1964,1968,1972,1976,1980,1984,1988,1992,1996,2000,2004]
Vemos que las tres funciones operan correctamente en el rango de números dados 2).
Para realizar en lo que resta de la clase.
3/4
como (3,4)
, representamos 5/2
como (5,2)
, etc. No es necesario realizar ninguna simplificación al resultado.
probar con (1,2) y (1,2), (1,4) y (1,4).
probar con (0,1), (2,2), (3,1).
probar con 5 y 9, con -8 y 9, con -10 y -1, con 0 y 0 y con 0 y 3
Definir la función edad : (Int, Int, Int) → (Int, Int, Int) → Int que dadas dos fechas indica los años transcurridos entre ellas. Por ejemplo edad.(20,10,1968).(30,4,1987) = 18. Suponer que las fechas están siempre bien formadas y que la primera es menor o igual a la segunda.
probar con (16,4,1980) y (17,5,1992), (16,4,1980) y (14,5,1992), (16,4,1980) y (15,4,1992) y con (16,4,1980) y (17,5,1972).
En un prisma rectangular, llamemos h a la altura, b al ancho y d a la profundidad. Completar
la siguiente definición del área del prisma:
area.h.b.d = 2 ∗ frente + 2 ∗ lado + 2 ∗ arriba
|[ …aca va la definicion… ]|
donde frente, lado y arriba son las caras frontal, lateral y superior del prisma respectivamente.
Completar la función area.h.b.d area : Int → Int → Int → Int que calcula el área de un prisma rectangular:
area :: Int -> Int -> Int -> Int area.h.b.d = 2*frente + 2*lado + 2*tapa where frente = ... lado = ... tapa = ...