7 de diciembre de 2007

Simplificar Funciones en Haskell

Hoy voy a hablar un poco acerca de un proyecto propio, y de como usar las características de un lenguaje como Haskell para escribir menos. El proyecto es una calculadora de pila (primero se escriben los operandos y luego se escribe el operador) y la podéis encontrar en Hscalc.

El código que viene a continuación son las funciones ejecutadas cuando se pulsa sobre los diferentes botones de la calculadora. Por ejemplo la función pulsaNumero inserta un nuevo dígito en la pila.

pulsaNumero v entries n = do
val <- readIORef v
let newVal = insertaDigito val n
writeIORef v newVal
putStackInEntries entries newVal

pulsaComa v entries = do
val <- readIORef v
let newVal = insertaComa val
writeIORef v newVal
putStackInEntries entries newVal

pulsaSigno v entries = do
val <- readIORef v
let newVal = insertaSigno val
writeIORef v newVal
putStackInEntries entries newVal

pulsaStackAdd v entries = do
val <- readIORef v
let newVal = nullValue:convertValues val
writeIORef v newVal
putStackInEntries entries newVal

pulsaStackClear v entries = do
writeIORef v pilaVacia
putStackInEntries entries pilaVacia

pulsaOpBinaria v entries f = do
val <- readIORef v
let newVal = aplicaFuncion val f
writeIORef v newVal
putStackInEntries entries newVal

Podemos ver que se repite un mismo patrón en todas las funciones. Primero se obtiene el valor actual de la calculadora, se aplica una función que modifica este valor y luego se guarda el nuevo valor y se actualiza la ventana. Con este patrón nos hacemos nuestra función base, la cual toma como primer parámetro la función a aplicar para modificar el estado de la calculadora. Esta función base es pulsaFuncion.

pulsaFuncion funcion v entries = do
-- cojer valor actual
val <- readIORef v
-- aplicar una modificacion al valor
let newVal = funcion val
-- actualizar valor
writeIORef v newVal
-- actualizar ventana
putStackInEntries entries newVal

La primera característica que vamos a aprovechar de Haskell es la aplicación parcial. Con esta característica, podemos definir nuevas funciones fijando el valor de parte de los parámetros. De esta manera definimos pulsaComa y pulsaFuncion, fijando el primer parámetro. Al resto de los parámetros se les da valor a la hora de invocar las nuevas funciones.

pulsaComa = pulsaFuncion insertaComa
pulsaSigno = pulsaFuncion insertaSigno

Para el resto de funciones tenemos que usar funciones anónimas. Una función anónima permite definir funciones en cualquier lugar en donde se pueda usar una expresión. En Haskell, las funciones anónimas se definen de la siguiente manera.

-- función anónima con dos parámetros a y b
(\a b-> a+b)

Usando una función anónima definimos pulsaStackAdd con una función que dado un valor, le inserta un cero al principio.

pulsaStackAdd =
pulsaFuncion (\v-> nullValue:convertValues v)

Para la función pulsaStackClear, ignoramos el parámetro y siempre devolvemos una pila vacía. Aunque el código original de pulsaStackClear era menor, el resultado es el mismo al ser Haskell un programa de evaluación perezosa. No realiza la llamada para obtener el valor actual de la calculadora ya que luego no va a usar ese valor.

pulsaStackClear = pulsaFuncion (\_-> pilaVacia)

Para las funciones pulsaNumero y pulsaOpBinaria necesitamos un parámetro adicional, el resto es igual que en los casos anteriores.

pulsaNumero n = pulsaFuncion (\v-> insertaDigito v n)
pulsaOpBinaria f = pulsaFuncion (\v-> aplicaFuncion v f)

Con estos cambios el código se simplifica enormemente, aumentando en legibilidad y siendo mucho más fácil de mantener. Si queremos hacer algo adicional, con cambiar la función base nos bastaría.

pulsaFuncion funcion v entries = do
val <- readIORef v
let newVal = funcion val
writeIORef v newVal
putStackInEntries entries newVal

pulsaComa = pulsaFuncion insertaComa

pulsaSigno = pulsaFuncion insertaSigno

pulsaStackAdd =
pulsaFuncion (\v-> nullValue:convertValues v)

pulsaStackClear = pulsaFuncion (\_-> pilaVacia)

pulsaNumero n = pulsaFuncion (\v-> insertaDigito v n)

pulsaOpBinaria f = pulsaFuncion (\v-> aplicaFuncion v f)