Programación Aplicativa

Publicado el 1 de marzo de 2017 por pepegar

Este post es una traducción al castellano del que se puede encontrar en http://pepegar.github.io/2016/09/applicative-style-programming.html

Programación Aplicativa

En este post vamos a ver qué son los funtores aplicativos, un poco más que los funtores, un poco menos que las mónadas. Y, si tenemos mónadas, ¿para qué queremos funtores? La respuesta es que al proveer menos funcionalidad, los podremos usar en más ocasiones.

Este post intenta complementar la charla que dimos en HaskellMAD, puedes ver los slides de la charla en este enlace

Los funtores aplicativos se introdujeron en un artículo en The Journal of Functional Programming.

Lo que tenemos que entender es que los funtores aplicativos, como los funtores, las mónadas o los monoides, son una typeclass. Ésta es su definición:

class Functor f => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

Muy bien, ¿y qué es lo que tenemos aquí? Básicamente podemos ver que Applicative es una interfaz que declara dos funciones. La primera de ellas pure, permite meter elementos dentro de nuestro contexto aplicativo. La otra <*>, es una funcion infija que permite aplicar la una funcion a -> b contenida en un contexto aplicativo a un valor a contenido en el mismo contexto, dando como resultado un valor f b. Después de el trabalenguas, una aclaración, podemos pensar en los funtores aplicativos como aplicación de funciones dentro de un contexto.

Veamos un ejemplo sencillo del uso de Applicative.

Prelude> let square = \ x -> x * x
Prelude> let plusTwo = \ x -> x + 2
Prelude> let functions = [square, plusTwo]
Prelude> let numbers = [1,2,3]
Prelude> functions <*> numbers
[1,4,9,3,4,5]

En el ejemplo vemos cómo las funciones contenidas dentro de la lista functions son aplicadas a la lista numbers y el resultado es concatenado. La librería estandard de Haskell nos ofrece instancias para la mayoría de tipos de datos, y se importan automáticamente en Prelude, por eso no necesitamos hacer ningún import para que el ejemplo funcione.

De todos modos, no necesitamos ser genios para ver que una lista de funciones no es el ejemplo más util del mundo. Veamos un ejemplo más interesante:

Validación

La validación de datos es un problema que todos, como programadores, nos hemos encontrado. Tenemos datos que van a entrar a nuestro dominio, pero antes de hacerlo queremos comprobar que son válidos. Al comprobar si un valor de tipo a es válido, nuestros validadores devolverán el valor en caso de que lo sea, o un error de tipo Err en caso de que no lo sea. Dado que estos validadores devolverán un tipo u otro, vamos a usar el tipo de datos Either de Haskell.

Para entenderlo un poco, Either es un tipo suma. Esto quiere decir que los el número de valores posibles para un dato del tipo Either a b es el número de valores posibles de a, mas el número de valores posibles de b.

data Person = Person {
  name :: String,
  age :: Int 
} deriving Show

data Err = String

validateName :: String -> Either Err String
validateName n = if (onlyLetters n) then Right n
                                    else Left "El nombre debe contener letras únicamente"

validateAge :: Int -> Either Err Int
validateAge a = if a > 18 then Right a
                          else Left "Tienes que ser mayor de 18 años"

En nuestro ejemplo, tenemos un tipo de datos `Person`, que representa a una persona con nombre y edad. Y tenemos dos validadores, uno para cada componente de este tipo de datos.

Ahora, con nuestros dos validadores, podemos crear una validación para Person si usamos notación do:

validatePersonM :: String -> Int -> Either String Person
validatePersonM n a = do
    vName <- validateName n
    vAge <- validateAge a
    return $ Person vName vAge

Lo que estamos haciendo aquí es muy sencillo. Dado que Either provee una instancia de Monad, podemos utilizar la notación do para secuenciar operaciones monádicas sobre este tipo de datos. Pero, hay una cosa que debemos preguntarnos: ¿Realmente queremos que nuestras operaciones sean secuenciales? Cuando la respuesta a esta pregunta sea No, sabemos que estamos ante un claro caso de UIDM, o uso indiscriminado de Monad.

Para resolver nuestro UIDM, y sabiendo que Either aparte de proveer una instancia de mónada, provee otra para Applicative, podemos cambiar nuestras operaciones secuenciales a operaciones aplicativas.

validatePersonA :: String -> String -> Either String Person
validatePersonA n l = Person <$> validateName n <*> validateLastName l

Pero, ¿cómo funciona esta función? Bueno, simplemente hay que entender que el operador <$> es un alias infijo para el fmap de los funtores. Es decir, la parte izquierda del operador es mapeada sobre el contenido de nuestro funtor.

En esta sesión de GHCi lo podemos ver mejor:

Prelude> :t Person
Person :: String -> String -> Person
Prelude> :t Person <$> validateName "pepe"
Person <$> validateName "pepe" :: Either String (String -> Person)
Prelude> :t Person <$> validateName "pepe" <*> validateLastName "Garcia"
Person <$> validateName "pepe" <*> validateLastName "Garcia" :: Either String Person

Creando nuestros propios aplicativos

Para usar toda la magia de Applicative con el tipo de datos que nosotros queramos, sólamente hay que dar una instancia de la typeclass:

data Maybe a = Just a
             | Nothing


instance Applicative Maybe where
    pure = Just

    Just f  <*> m = fmap f m
    Nothing <*> _ = Nothing

Esto nos permitirá usar Maybe como un aplicativo, pero hay otros casos, como por ejemplo, ¿cómo puedo hacer para usar toda la magia de los aplicativos sobre un ADT que hayamos creado? En Haskell existe una técnica para esto que se denomina ~Free Applicatives~. En este caso, Free se refiere a gratis, no a libre, y es así porque nos da una instancia de Applicative gratuíta para nuestro ADT.

Free Applicatives

Los Free Applicative son una abstracción sobre Applicative, y como funcionan básicamente, es elevando a representaciones de tipos las funciones que expone la typeclass de Applicative.

Puedes investigar más sobre la implementación en Github, pero básicamente, los Free Applicative se definen así:

data Ap f a where
    Pure :: a -> Ap f a
    Ap   :: f a -> Ap f (a -> b) -> Ap f b

instance Functor (Ap f) where
  fmap f (Pure a)   = Pure (f a)
  fmap f (Ap x y)   = Ap x ((f .) <$> y)

instance Apply (Ap f) where
  Pure f <.> y = fmap f y
  Ap x y <.> z = Ap x (flip <$> y <.> z)

instance Applicative (Ap f) where
  pure = Pure
  Pure f <*> y = fmap f y
  Ap x y <*> z = Ap x (flip <$> y <*> z)

Ahora, con el constructor Ap, tenemos una instancia de Applicative para cualquier tipo de datos * -> *!

Para intentar dar un ejemplo, vamos a modelar un blog mediante Ap.

{-# LANGUAGE GADTs #-}

import Control.Applicative.Free
import Control.Applicative

type Id = String

data Author = Author {
    name :: String,
    lastName :: String
} deriving Show

data Post = Post {
    id :: Int,
    title :: String,
    content :: String,
    excerpt :: String
} deriving Show

data BlogF a where
    GetPost :: Id -> BlogF Post
    GetAuthor :: Id -> BlogF Author

type Blog a = Ap BlogF a

Lo más importante del ejemplo es entender que en este caso, cuando definimos nuestro GADT, estamos definiendo nuestro lenguaje, o DSL de operaciones de blog.

También, para no tener que estar lidiando con los constructores de datos todo el rato, vamos a crear lo que se denomina como smart constructors, o constructores inteligentes que nos eleven un valor del tipo `BlogF` al tipo `Blog`.

Para hacer esto (elevar un valor a `Ap`) la librería que usamos nos da una función llamada liftAp.

getPost :: Id -> Blog Post
getPost id = liftAp $ GetPost id

getAuthor :: Id -> Blog Author
getAuthor id = liftAp $ GetAuthor id

Ahora lo más divertido, vamos a crear un programa que imprima una página de nuestro blog!

data Page = Page {
    post :: Post,
    author :: Author
} deriving Show

getPage :: Id -> Id -> Blog Page
getPage postId authorId = Page <$> getPost postId
                               <*> getAuthor authorId

¡Increíble! dado que no hay dependencias entre obtener un autor por id, y obtener el post por id, podemos componer una página de manera idiomática1!

Pero hay una parte muy importante del blog que estamos olvidando, y es .

Por ahora no estamos escribiendo nada por la pantalla, ni yendo a la base de datos a obtener nuestros posts. Sólamente estamos creando nuevos valores y devolviéndolos. Estamos creando un Árbol de Sintáxis Abstracta, o AST.

Para evaluar nuestro AST, tenemos que usar un concepto nuevo, llamado Transformación Natural. Aunque tenga un nombre tan increíblemente difícil, lo que tenemos que saber de las Transformaciones Naturales es que son funciones con la siguiente forma:

natTrans :: BlogF a -> IO a

Es decir que en vez de transformar el contenido, como hace fmap por ejemplo, transforma el contenerdor, manteniendo el contenido. ¿Por qué queremos interpretar a IO? porque en Haskell, todos los side effects ocurren en IO.

Ahora, vamos a crear el intérprete para las operaciones BlogF!

interpret :: BlogF a -> IO a
interpret (GetPost id)   = putStrLn ("getting post " ++ show id ++ " from DB")   *> pure $ Post id "this is the post" "content of the post" "excerpt"
interpret (GetAuthor id) = putStrLn ("getting author " ++ show id ++ " from DB") *> pure $ Author "Pepe" "García"

Y, finalmente, vamos a interpretar nuestros programa de renderización del blog!

main :: IO ()
main = do
    page <- runAp interpret $ getPage 1 1
    print page

-- Output:
-- getting post 1 from DB
-- getting author 1 from DB
-- Page {post = Post {id = 1, title = "this is the post", content = "content of the post", excerpt = "excerpt"}, author = Author {name = "Pepe", lastName = "Garc\237a"}}

runAp es una función de la libería free que toma un programa basado en FreeAp y lo interpreta mediante una Transformación Natural.

Análisis estático

Una de las cosas más interesantes que podemos hacer con ASTs aplicativos, es análisis estático. El análisis estático es una técnica mediante la cual podemos sacar conclusiones de un programa sin evaluarlo.

La razón por la que los aplicativos permiten análisis estático, mientras que las mónadas no, es porque en los aplicativos no hay que evaluar ninguna expresión para conocer el árbol sintáctico completo. En cambio, como en las mónadas necesitamos evaluar las llamadas a >>= para obtener la siguiente expresión.

Ahora imaginemos que queremos limitar el número de llamadas que nuestro blog hace a la base de datos. Dado que sabemos que en nuestro intérprete todas las operaciones GetAuthor y GetPost se interpretarán como una sentencia SQL, podemos simplemente contar el número de operaciones de este tipo.

Cómo conseguimos esto?

instance Monoid Int where
    mempty = 0
    mappend = (+)

countInstructions :: BlogF a -> Int
countInstructions _ = 1

main :: IO ()
main = do
    putStrLn "NUMBER OF REQUESTS TO THE DB:"
    print instructions
    where instructions = runAp_ countInstructions page
          page = getPage 1 1

-- Output:
--
-- NUMBER OF REQUESTS TO THE DB:
-- 2

runAp_ es una versión modificada de runAp que toma como intérprete una función:

fn :: Monoid b => f a -> b

Que aplica mappend a todos los b producidos en nuestro intérprete.

Conclusiones

Hay que identificar cuándo usar Aplicativos. Es un poco complicado al principio, dado que no estamos habituados a trabajar con ellos, pero como regla, podemos pensar que los aplicativos nos ayudan cuando nuestras operaciones no dependen unas de las otras.

Footnotes


  1. En un principio, los Applicative se denominaron Idioms.