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)
No hay comentarios:
Publicar un comentario