Manteniendo elementos en tiempo de ejecución en un programa Haskell

Introducción y bienvenida

Les presento a mi recién nacido blog🙂, un espacio donde voy a tratar de compartir algunos conocimientos que espero sean de utilidad a la mayor cantidad posible de personas. Mayormente estaría posteando cosas relativas a programación y temas afines.

En este primer post voy a estar tratando el problema de mantener elementos (de algún tipo) durante la ejecución de un programa escrito en Haskell. Al escribir esto, estoy asumiendo que ya tienes un conocimiento considerable sobre el lenguaje, sus construcciones, librerías, entornos, etc. Si no tienes este conocimiento es buen momento para aprender este interesante lenguaje, el punto de partida es la página principal de Haskell.

El problema y un enfoque para solucionarlo

Debido a que Haskell es un lenguaje de programación puramente funcional, nuestros programas estarán compuestos únicamente por funciones, y la manera en que vamos resolviendo problemas es mediante las llamadas a estas funciones. En un lenguaje funcional, el concepto de imperatividad se cambia por la aplicación de funciones a parámetros, esto es, al programar nos centramos en describir lo que queremos hacer, y no cómo queremos que se haga. Además de esto, como no estamos modificando el estado del programa, éste carece de variables, como normalmente se conocen en los lenguajes de programación imperativos.

A esto viene el planteamiento del problema: cómo mantener elementos disponibles siempre en tiempo de ejución entre las llamadas a las funciones? Muchas veces, necesitamos tener disponibles un conjunto de elementos durante el transcurso de la ejecución para poder utilizarlos, y adicionalmente tal vez deseemos tambien modificarlos, agregar otros, elminarlos, etc.

Un ejemplo de este patrón sería manejar una “tabla de símbolos” para un intérprete sencillo, digamos un evaluador aritmético, que permita definir nombres que puedan tener valores asociados (variables), y que tengan que estar disponibles para su utilización posterior. O digamos tal vez que implementamos un gestor de citas en un calendario, en donde las citas, deben estar disponibles para su chequeo, para posponerlas a otra fecha del calendario, o para descartarlas, etc.

Bien, en este punto identificamos los elementos (variables y citas) que necesitamos disponibles, como necesitamos un conjunto de ellos, debemos buscar la manera de que esté disponible una lista de estos elementos. Gracias a que en Haskell nos manejamos con llamadas a funciones, podemos utilizar el paso de parámetros y la recursividad para plantear una solución a la problemática.

Aplicación paso a paso: linedit, un editor de texto sencillo

En este ejemplo, haremos un editor de texto muy (pero muy) simple (y a pesar de esto, funcional). Para editar un texto, necesitamos tener disponible obviamente el texto mismo, pero para poder aplicar el patrón identificaremos a cada linea como un elemento, de esta manera, nuestra intención será mantener una lista de lineas del texto que estemos editando durante la ejecución.

Nuestro editor será capaz de:

  • Cargar un archivo para su edición
  • Mostrar el archivo en pantalla
  • Guardar el archivo con un nombre dado
  • Agregar texto al final del archivo
  • Borrar lineas del archivo
  • Editar lineas del archivo

Esqueleto principal del programa

Escribimos la función principal, con una cabecera de bienvenida y una llamada a otra función que se encargará de recibir los comandos y mantener la lista en tiempo de ejecución:

1	main = do
2	  imprimirCabecera
3	  main_ []
4
5	main_ xs = do
6	  cmd <- prompt "linedit> "
7	  let comando = trim cmd
8	  case comando of
9	    "help" -> do
10	             mostrarAyuda
11	             main_ xs
12	    "?"    -> do
13	             mostrarAyuda
14	             main_ xs
15	    "quit" -> return ()
16	    ""     -> main_ xs
17	    otherwise -> do
18	                newXs <- catch (evaluar comando xs) manejador
19	                main_ newXs
20	                where
21	                  manejador e = do
22	                    putStrLn ("Error al evaluar \"" ++ comando ++ "\"" )
23	                    print e
24	                    return xs

En 3, llamamos a main_ con una lista vacía, pues inicialmente no estamos editando ningún archivo, pero si nos fijamos mejor, podemos ver el truco en 11, 14 y 16. En esos puntos estamos haciendo una llamada recursiva a main_ con el mismo parámetro que recibió luego de haber ejecutado el comando (help, ?, y comando vacío respectivamente), ya que estos comandos no modifican las líneas que guardaremos. Por otro lado, es más interesante 18 y 19, donde vemos que hay una llamada a la función evaluar, la cual toma los comandos que sí modificarán la lista (editar, agregar, etc.) y retorna una nueva lista con las modificaciones, por lo que en la siguiente llamada a main_ se le debe pasar newXs. Un caso particular ocurre si surge alguna excepción, teniendo que ejecutarse el manejador, en este caso termina devolviéndose la lista sin modificación alguna.

Pasemos a ver ahora la funcion evaluar:

1	-- evaluar: Recibe un comando del prompt y lo interpreta
2       -- llamando a la funcion correspondiente.
3	evaluar cmd xs = do
4	   let tokens = words cmd
5	   case (tokens !! 0) of
6	     "mostrar" -> do
7	                 mostrarArchivo xs
8	                 return xs
9	     "cargar"  -> do
10	                 archivo <- catch (cargarArchivo (tokens !! 1)) handler
11	                 let newXs = numerarLineas archivo
12	                 return newXs
13	                   where handler e = do
14	                           putStrLn ("No se pudo leer \"" ++ tokens !! 1 ++ "\"")
15	                           print e
16	                           return []
17	     "guardar" -> do
18	                 guardarArchivo (tokens !! 1) xs
19	                 return xs
20	     "agregar" -> return (agregarLinea (unwords (tail tokens)) xs)
21	     "editar"  -> return (editarLinea (read (tokens !! 1))
22	                                      (unwords(drop 2 tokens))
23	                                       xs)
24	     "borrar"  -> return (borrarLineas (read (tokens !! 1))
25	                                       (read (tokens !! 2))
26	                                       xs)
27	     otherwise -> do
28	                 putStrLn $ "No reconozco este comando: \"" ++ tokens !! 0 ++ "\""
29	                 return xs

Dentro de evaluar, como se puede ver, ya no hay llamadas a main_, sin embargo, hay que notar que las funciones de edición necesitan las líneas, y luego deben retornar el resultado de su operación, es por eso que en 20, 21 y 24 las llamadas a agregarLinea, editarLinea y borrarLineas todas reciben respectivamente como parámetro a la lista, y terminan devolviéndola modificada para que de esta manera siempre estén disponibles.

Para ir adentrándonos más en el código de nuestro programa, vamos a ver una de las funciones que editan nuestra lista, en este caso agregar:

1	-- agregarLinea: Agrega una linea al final del archivo
2	agregarLinea linea xs = xs ++ [(n, linea)]
3	  where n = (fst (last xs)) + 1

Algo raro que notar? Hasta este momento no hemos dicho el tipo exacto de los elementos de nuestra lista, pudieron haber sido simples Strings, pero para ejemplificar mejor se optó por una lista de tuplas de un entero y un String, donde cada entero es el número de la línea en tiempo de ejecución (así también se puede ver mejor como se aplicaría a los elementos {nombre, valor} para el caso de las variables y digamos {fecha, cita} para el ejemplo del calendario). Lo que hacemos en esta función es simplemente encontrar el número de la última línea, sumarle 1, y concatenar como una tupla (el número y la línea) a la lista recibida inicialmente.

De esta forma, podemos ver como siempre tenemos disponible algún “xs”, es decir, alguna lista a la que siempre podemos acceder para hacer las modificaciones que necesitemos.

Ventajas y Desventajas

Este enfoque dista mucho de ser el mejor posible, como ventaja tenemos que es muy fácil comprenderlo, es decir, consta nada más de un parámetro adicional a las funciones que van a necesitar la lista aprovechando la posibilidad de llamadas recursivas si fuera necesario. Por otro lado, no es muy “escalable”, es decir, en funciones o módulos sencillos es perfectamente manejable y hasta cómodo si se quiere, pero ni pensar en utilizarlo en un programa con una cantidad considerable de funciones, pues la depuración sería dificultosa.

El código completo de linedit lo pueden bajar de aquí, está publicado bajo la licencia GPL, por lo que pueden disponer del código para lo que gusten (si es que les sirve :P), cualquier bug, mejora, sugerencia o preguntas que puedan surgir pueden agregar un comentario.

6 Responses to “Manteniendo elementos en tiempo de ejecución en un programa Haskell”


  1. 1 Cesar Rodas diciembre 31, 2008 a las 11:28 pm

    Bienvenido a la blogosfera!

    Este año tuve que solucionar un problema similar en haskell, para ello se me ocurrió una mejor solución (en teoría). Tenía que hacer un programa sea una calculadora y que guarde variables.

    Mi idea fue utilizar archivos para guardar las variables y sus valores, mi idea era guardar en las líneas impares las variables y en las pares sus valores, ej:

    foo
    5
    bar
    6

    Como mencioné antes, la solución solo fue en teoría porque no me funciono, por cuestión de transformaciones de tipos de datos, ya que los archivos abiertos en haskell no retornan un string (creo que retorna IOStringo algo así), tenes alguna idea de como se podría solucionar eso, mas que nada como para aprender, porque ya es tarde😉.

  2. 2 Alberto G. enero 1, 2009 a las 3:26 am

    Qué bueno el blog eliseo … no sabia que podías escribir😛 … qué fanático es Cesar como para dejar un comentario un 31 de diciembre antes de la media noche … andá farrea!!!

  3. 3 eocampos enero 4, 2009 a las 11:46 pm

    Cesar: La verdad no hay necesidad de complicarse, si tu intención era modificar el archivo de “variables”, la idea sería tomar el contenido del archivo en memoria y manipularlo como si fuera una lista. Fijate como lo hago en ‘linedit’:
    — cargarArchivo: Carga nombreArchivo para su edición
    cargarArchivo nombreArchivo = do
    l <- readFile nombreArchivo
    return (lines l)

    Luego ‘l’ es una lista, donde cada elemento corresponde a cada linea del archivo. De aquí en adelante ya te manejás con los elementos nomas y te olvidas de IO String.

    La idea esta buena, sólo que si tenemos que acceder a muchas variables a menudo podría haber alguna especie de sobre carga o bajo rendimiento si es que estamos leyendo y leyendo el archivo de variables.

    Alberto: Gracias! (vos no te quedás atrás, respondiste a las 03:26am!) jaja

  4. 4 guiyiman febrero 11, 2009 a las 3:31 am

    esto si que es una inesperada casualidad,, fijate a quien encuentro por aca a “eliseo:insectus” abrazos desde madrid soy el gran guillin,jejeje

  5. 5 Jhon agosto 17, 2009 a las 3:25 pm

    Realmente buen aporte Cesar, es verdad como ventaja pues tenemos que es muy fácil comprenderlo, este lenguaje es muy bueno, aunque tiene ciertas limitaciones, pero es muy recomendable por el aspecto funcional.
    Algunos ejemplos se muestran en:
    http://www.program.webcindario.com/codigos/haskell.html

  6. 6 eocampos agosto 17, 2009 a las 3:33 pm

    Gracias por los aportes Jhon!


Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s




My Tweets!

Error: Twitter no responde. Por favor, espera unos minutos y actualiza esta página.


A %d blogueros les gusta esto: