[F#] Free monad interpreter, DSL y DDD

Hace ya poco más de dos años leí éste post, pero, en aquel momento no le encontré tanta utilidad (más por ignorancia que por otra cosa) y dejé el tema ahí, como algo interesante que, por lo pronto, no usaría en F#. No fue si no hasta finales del año pasado, leyendo otro articulo o una pregunta en SO (no recuerdo) donde se mencionó el termino Free Monad Interpreter que vi relacionados varios conceptos y tuve uno de esos momentos mentales

happy excited shocked awesome surprised

He dudado en escribir esta entrada porque aún hay detalles que se escapan a mi entendimiento y experiencia, pero sé que al escribirlo lo entenderé, al menos, un poco mejor, así que aquí va.

Lenguaje Ubicuo

Siempre que se habla de DDD se habla del lenguaje ubicuo que, cómo Martin Fowler lo expone, “es el término que Eric Evans usa en DDD para la práctica de construir un lenguaje común y riguroso entre desarrolladores y usuarios.” (traducción propia). En mi experiencia personal, y hablando desde la perspectiva del código en lenguajes imperativos (C# en mi caso particular), la construcción de éste lenguaje se limita a la representación en clases y métodos bien nombrados, “nutriéndolo”, el modelo de dominio, para que no sea anémico y que, cuando el usuario diga algo en su lenguaje natural seamos capaces de mapearlo a ésta representación nuestra. Pero lo que veo ahora es que este lenguaje natural puede ser también el lenguaje en el que se representen los requerimientos en nuestro código. Podemos traducir este lenguaje ubicuo a un DSL o mini-lenguaje que nos permitirá no traducir si no casi transcribir los requerimientos en nuestro código.

Free Monads

De nuevo esa extraña palabra aparece, pero en esta ocasión, no está sola. Qué es un Free Monad, teóricamente hablando (teoría de categorías y demás), se escapa de mi conocimiento. Pero me he hecho una imagen practica basada, como raro, en blogs y respuestas en SO. “Un Free Foo pasa a ser la cosa más simple que satisface todas las leyes del ‘Foo‘. Es decir que satisface exactamente las leyes necesarias para ser un Foo y nada extra.”1 esto es, el Free Monad es un monad (cumple sus leyes), pero no realiza ningún calculo, definiendo así nada más los contextos a computar2  En esta parte de quien y cómo computa (con qué monad) estos contextos es, cómo yo lo veo, donde viene la parte de los interpretadores del titulo de esta entrada. Gabriel Gonzalez tiene un excelente articulo explicando precisamente por qué importan los Free Monads, inicia su entrada de manera triunfal (traducirlo, pienso, lo arruinaría):

Good programmers decompose data from the interpreter that processes that data. Compilers exemplify this approach, where they will typically represent the source code as an abstract syntax tree, and then pass that tree to one of many possible interpreters.

Entre las ventajas de esta separación está la posibilidad de crear varios interpretadores separando así las acciones puras de las impuras (IO) y facilitando así las pruebas a nuestro código.

Implementación en F#

Sabemos que en F# existe el concepto de computation expressions y hasta donde sé no hay un computation expression para los Free Monads. Mostrar aquí su implementación dese cero sería caer en el error de la mera traducción del articulo mencionado al inicio de esta entrada. Por lo que espero que el lector se tome su tiempo para entender lo que allí se expone pues ese mismo código será empleado para desarrollar mi ejemplo.

Supongamos ahora un caso de uso como el siguiente: En el registro de nuevos usuarios se debe validar que la contraseña tenga una longitud mayor a seis y que el usuario no exista. Si se cumplen estas condiciones se registra el usuario, de lo contrario se muestra un mensaje indicando el error.

Con esta información procedemos a crear los tipos correspondientes

type User = {
    username : string
    password : string
}

type Error = string

Ahora definimos las acciones que se detallan en el caso de uso, esto es, consultar si el usuario existe, crear un usuario y notificar al usuario:

type UseCase<'a> =
    | Notify of Error * 'a
    | UserExist of User * (bool ->  'a)
    | Register of User * 'a

let mapUseCase (f : 'a -> 'b) (useCase : UseCase<'a>) : UseCase<'b> =
    match useCase with        
        |  Register (user, value) -> Register (user, f value)
        |  UserExist (user, fn)  -> UserExist (user, f << fn) 
        | Notify (message, v) -> Notify (message, f v)

Puede surgir la inquietud de por qué la validación no hace parte de este conjunto de acciones. Si bien podemos agregarla y trabajar con ella esta validación no presentaría ningún cambio entre los interpretes que ya veremos. Esta validación puede hacer uso del Option o bien de una monad Result<success,error> pero no cambiará el cómo es interpretada (puro/IO).

En el código del Free Monad solo he renombrado el código del ejemplo del post mencionado:

type FreeUseCase<'a> =
    | Pure of 'a
    | Free of UseCase<FreeUseCase<'a>>

let rec bind (f : 'a -> FreeUseCase<'b>) (useCase : FreeUseCase<'a>) : FreeUseCase<'b> =
    match useCase with
        | Pure value -> f value
        | Free t -> Free (mapUseCase (bind f) t)

let liftF (useCase:UseCase<'a>) : FreeUseCase<'a> = Free (mapUseCase Pure useCase)

let (>>=) = fun useCase f -> bind f useCase

let (>>.) = fun t1 t2 -> t1 >>= fun _ -> t2

Las funciones “elevadas” que nos permitirán construir el DSL:

let register (user : User) : FreeUseCase<unit> = liftF (Register (user, ()))
let notify (message : Error) : FreeUseCase<unit> = liftF (Notify (message, ()))
let userExist (user : User) : FreeUseCase<bool> = liftF (UserExist (user, true |> (=)))

Con este código ya podemos traducir el requerimiento y quedaría algo como:

let createUser' user = 
    userExist user
    >>= fun exists -> 
        if exists then 
            validatePassword user.password 
            |> function
            | Some _ -> register user
            | None -> notify "invalid password"
        else
            notify "username already exists"

Aquí se hace notoria, en mi humilde opinión, una de las limitaciones de F# al no poder generalizar los monads (polimorfismo de orden superior) y poder aplicar el fmap entre estos.

Vemos que los nombres de las funciones se corresponden con las acciones indicadas por el usuario, pero puede resultar confuso tantos símbolos indicando el flujo del programa, para esto podemos crear un computation expression y expresarlo de manera imperativa, más similar al lenguaje ubicuo. El computation expression quedaría, de nuevo tomando el código del ejemplo anterior, algo como:

type UseCaseBuilder() =
    member x.Bind(term, f) = bind f term
    member x.Return(value) = Pure value
    member x.Combine(term1, term2) = term1 >>. term2
    member x.Zero() = Pure ()
    member x.Delay(f) = f()
let usecase = new UseCaseBuilder()

Y su uso:

let createUser user = usecase {
    let! exist = userExist user
    if (not exist) then   
        match (validatePassword user.password) with
        | Some _ -> do! register user
        | None -> do! notify "invalid password"
    else
        do! notify "username already exists"
}

Ahora bien, como decíamos en la definición de los free monad hace falta el monad que reduzca (compute) estas expresiones, para ello vienen los interpretadores. En nuestro caso usaremos dos, uno puro, usando la monad List y uno impuro, usando IO (printfn).

let rec interpretIO (useCase:FreeUseCase<'a>) : 'a =
    match useCase with
        | Free (Register (user, next)) -> 
            printfn "User: %s Pass: %s" user.username user.password
            interpretIO next
        | Free (UserExist (user, fn)) -> 
            // should be from DB, u know... 
            interpretIO (fn (pureExist user.username))
        | Free (Notify (error, next)) -> 
            printfn "Error: %s" error
            interpretIO next
        | Pure a -> a

let rec interpretPure (acc : string list) (useCase:FreeUseCase<'a>) : string list =    
    match useCase with
    | Free (Register (user, next)) -> 
         interpretPure ((sprintf "User: %s Pass: %s" user.username user.password) :: acc) next
        | Free (UserExist (user, fn)) -> 
            interpretPure acc (fn (pureExist user.username))
        | Free (Notify (error, next)) ->             
            interpretPure ((sprintf "Error: %s" error) :: acc) next
    | Pure a -> acc

Con esta flexibilidad podemos probar con el interprete puro, y hacer pruebas de integración con otro interprete…

> interpretPure [] (createUser {username = "nicolas"; password = "123456"});;
val it : string list = ["User: nicolas Pass: 123456"]
> interpretPure [] (createUser {username = "hugo"; password = "123456"});;
val it : string list = ["Error: username already exists"]
> interpretPure [] (createUser {username = "nicolas"; password = "123"});;
val it : string list = ["Error: invalid password"]

Haskell Bonus

En el mismo blog hay un ejemplo usando haskell, mucho más sencillo y limpio que el de F#. Yo hice un gist para mostrar su uso interactuando con otra monad.

Conclusiones y apuntes finales

  • Al ser F# un lenguaje funcional no puro, implementar técnicas como esta permite una separación que, si bien requiere atención por parte del programador, hace que se vea más clara la intención de dicha separación.
  • En esto de la construcción del DSL he intentado, sin mucho éxito, crear funciones y combinadores de la forma si – entonces – cuando que permitan una lectura del código como si fuera lenguaje natural. La mejor aproximación ha sido con el do notation/computation expression.
  • ¿Recuerdan CQS/CQRS? van bien de la mano. Aquí un post explicando cómo

El código completo de este post está en este gist

Referencias y enlaces de interés

1 Traducción de un fragmento de esta respuesta de Kmett en SO

2 Este hilo en general y esta respuesta en particular han sido usados para construir la definición que intenté presentar

What is the “Free Monad + Interpreter” pattern?

Puede ser que esto termine siendo una sobre ingeniería, claro esta. When would I want to use a Free Monad + Interpreter pattern?

Un post muy similar a este, con base de datos y todo

Anuncios
[F#] Free monad interpreter, DSL y DDD

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