IntroEl 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#