martes, octubre 14, 2008

Reader Monad en F#

Intro

El proyecto en el que trabajo usa en forma extensiva Linq para realizar todas las operaciones con la base de datos.

Éste es un ejemplo clásico de consulta:(1)


  112 let findById (db: MyProjectClassesDataContext, id) =

  113     SQL <@ {    for c in %db.Products

  114                 when c.ProductID = %id

  115                 -> c } @> |> Seq.hd



El ejemplo compila la expresión a un query nativo de SQL y lo ejecuta. El objeto por el cual nos "comunicamos" con la base de datos es el "DataContext" (En este caso le pusimos un nombre super enterprisey "MyProjectClassesDataContext" para darnos de importantes).

"DataContext" También nos ayuda a realizar cambios en forma transaccional. Por ejemplo:

  117 let changeDescription id text =

  118     let db = new MyProjectClassesDataContext()

  119     let product = findById(db,id)

  120     product.Description <- text

  121     db.SubmitChanges()



Cambia la descripción en forma transaccional.

Si tuviésemos ésta otra definición:


  123 let changeToLiquors id =

  124     let db = new MyProjectClassesDataContext()

  125     let product = findById(db,id)

  126     product.Type <- "Liquors"

  127     db.SubmitChanges()

  128 

  129 let changeDescriptionAndToLiquors id text =

  130     changeDescription id text

  131     changeToLiquors id



La creación de 2 SubmitChanges hace que hallan realmente 2 "transacciones" una puede realizarse con éxito pero la otra puede fallar. En algunos casos esto representa un peligro para la integridad de la base de datos. La forma correcta si queremos atomicidad en la operación sería crear un solo SubmitChanges para ambas operaciones.

Así mismo, los objetos de un DataContext, no pueden ser compartidos entre diferentes DataContext. Si digamos tengo los DataContext A y B, saco un producto de A y hago B.SubmitChanges(); el producto dado que pertenece a A, no es persistido en la base de datos.

Ésto nos obliga a tener un solo DataContext por transacción.

  117 et changeDescription db id text =

  118     let product = findById(db,id)

  119     product.Description <- text

  120 

  121 let changeToLiquors db id =

  122     let product = findById(db,id)

  123     product.Type <- "Liquors"

  124 

  125 let changeDescriptionAndToLiquors db id text =

  126     changeDescription db id text

  127     changeToLiquors db id



Incluso la última función la construimos recibiendo el DataContext como parámetro. Si estamos construyendo una librería ¿Quién somos para decir cuando queremos hacer persistencia? cada SubmitChanges e instanciación de DataContext es un problema si queremos componer después cada una de las funciones.

Pero ahora nos toca pasar SIEMPRE el DataContext, muchas veces toca hacer "types annotations". Y aceptemoslo... escribo a duras penas a 20 WPM. Y los nombres enterprisey no ayudan. A veces con aplicación parcial puede uno lograr ciertos ahorros pero está lejos de ser una solución.

Y bien?

Computations which read values from a shared environment.

Kurva jó!

F# posee azúcar sintáctica parecida a la sintaxis "do" de Haskell.

Definimos la monada(2)


   11 module Reader =

   12     type Reader<'e,'a> = Reader of ('e -> 'a)

   13     let apply (Reader f) r = f r

   14     let returnM x = Reader (fun e -> x)

   15     let bindM m f =

   16         Reader (fun e -> apply (f (apply m e)) e)

   17     let letM v f = bindM (returnM v) f

   18     let delayM f = bindM (returnM ()) f

   19 

   20     (*Reader Monad*)

   21     type MReaderBuilder() =

   22         member b.Return(x) = returnM x

   23         member b.Bind(v,f) = bindM v f

   24         member b.Delay(f) = delayM f

   25         member b.Let(v,f) = letM v f

   26 

   27     let reader = new MReaderBuilder()

   28     let ask = Reader (fun e -> e)

   29     let local f c = Reader (fun e -> apply c (f e))

   30     let asks sel = bindM (ask) (returnM << sel)

   31 



Internamente la monada no es mas que una función desde el "Entorno" hacia un "Retorno". Empecemos con ejemplos sencillos de como usar la monada.

Usando un string como contexto.


   35 let lengthOfString =

   36     reader {let! length = asks String.length 

   37             return length

   38            }



Simplifiquemos el contexto a un simple string. Esta definición nos retorna dado un contexto particular la longitud del mismo.

Para pasar el contexto:


   40 apply lengthOfString "hello"



(Si... retorna 4)

Ahora... A diferencia de Haskell, F# permite Side Effects. De esta manera lo siguiente también es posible(3).


   42 let lengthOfModifiedString =

   43     reader {let! length = lengthOfString

   44             let! lengthModified =

   45                 local

   46                     ((+) "Preffix") lengthOfString

   47             let! env = ask

   48             do printfn "%A" env

   49             return sprintf "%d: %d: %s"

   50                    length lengthModified env

   51            }



"asks" aplica la función al contexto.
"local" crea un contexto modificado(4) mediante una función.
"ask" retorna el contexto o entorno.

De la misma manera podemos usar el reader en casos mas complejos.


  103 type DBReader<'a> =

  104     Reader<MyProjectClassesDataContext, 'a>

  105 

  106 let getClients (db:MyProjectClassesDataContext) =

  107     db.Clients

  108 let getProducts (db:MyProjectClassesDataContext) =

  109     db.Products

  110 let getClientProducts (db:MyProjectClassesDataContext) =

  111     db.ClientsProducts

  112 

  113 let getClientByName1 name : DBReader<Person> =

  114     reader {let! clients = asks getClients 

  115             return List.find (fun (x) -> x.Name = name) clients

  116     }



Él código(5)

(1) Si mal no estoy la forma de realizar este tipo de consultas cambio con el CTP de F#

(2) Él nombre que se le da en F# son "computation expressions"

(3) El "do" para las expresiones que retornan unit creo que ya no es necesario con la última versión de F# (CTP Release).

(4) Toca recordar que estos es útil tratando con estructuras inmutables (i.e string).

(5) No usa la ultima versión de F#

1 comentario:

diegoeche dijo...

Si... que mierda como se ve el código... se veía bien en "vista previa"