Lenguajes específicos del dominio (domain-specific languages)
Objetivos del capítulo
En este capítulo exploraremos la implementación de lenguajes específicos del dominio (o DSLs) en PureScript, usando un número de técnicas estándar.
Un lenguaje específico del dominio es un lenguaje que es particularmente apropiado para desarrollar en un dominio de problemas concreto. Su sintaxis y funciones se eligen para maximizar la legibilidad del código usado para expresar ideas en ese dominio. Hemos visto varios ejemplos de lenguajes específicos del dominio en este libro:
- La mónada
Gamey sus acciones asociadas, desarrolladas en el capítulo 11, constituyen un lenguaje específico del dominio para el dominio del desarrollo de juegos de aventuras textuales. - La biblioteca de combinadores que escribimos para los funtores
AsyncyParalleldel capítulo 12 se pueden considerar un ejemplo de lenguaje específico del dominio para el dominio de la programación asíncrona. - El paquete
purescript-quickcheckcubierto en el capítulo 13 es un lenguaje específico del dominio para el dominio de verificación generativa. Sus combinadores permiten una notación particularmente expresiva para escribir propiedades de verificación.
Este capítulo tomará una aproximación más estructurada a algunas de las técnicas estándar para la implementación de lenguajes específicos del dominio. No es de ninguna manera una exposición completa del tema, pero debe proporcionarte suficiente conocimiento para construir algunos DSLs prácticos para tus tareas.
Nuestro ejemplo será un lenguaje específico del dominio para crear documentos HTML. Nuestro objetivo será desarrollar un lenguaje de tipos seguros para describir documentos HTML correctos, y trabajaremos mejorando en pequeños pasos una implementación ingenua.
Preparación del proyecto
El proyecto que acompaña este capítulo añade una nueva dependencia Bower; la biblioteca purescript-free que define la mónada libre (free monad), una de las herramientas que usaremos.
Probaremos el proyecto de este capítulo en PSCi.
Un tipo de datos HTML
La versión más básica de nuestra biblioteca HTML está definida el módulo Data.DOM.Simple. El módulo contiene las siguientes definiciones de tipos:
1 newtype Element = Element
2 { name :: String
3 , attribs :: Array Attribute
4 , content :: Maybe (Array Content)
5 }
6
7 data Content
8 = TextContent String
9 | ElementContent Element
10
11 newtype Attribute = Attribute
12 { key :: String
13 , value :: String
14 }
El tipo Element representa elementos HTML. Cada elemento consiste en un nombre de elemento, un array de pares de atributos y algún contenido. La propiedad de contenido usa el tipo Maybe para indicar si un elemento puede estar abierto (contiene otros elementos y texto) o cerrado.
La función clave de nuestra biblioteca es una función
1 render :: Element -> String
que representa elementos HTML como cadenas HTML. Podemos probar esta versión de la biblioteca construyendo valores de los tipos apropiados explícitamente en PSCi:
1 $ pulp repl
2
3 > import Prelude
4 > import Data.DOM.Simple
5 > import Data.Maybe
6 > import Control.Monad.Eff.Console
7
8 > :paste
9 … log $ render $ Element
10 … { name: "p"
11 … , attribs: [
12 … Attribute
13 … { key: "class"
14 … , value: "main"
15 … }
16 … ]
17 … , content: Just [
18 … TextContent "Hello World!"
19 … ]
20 … }
21 … ^D
22
23 <p class="main">Hello World!</p>
24 unit
Esta biblioteca tiene varios problemas tal cual está:
- Crear documentos HTML es difícil; cada nuevo elemento requiere al menos un registro y un constructor de datos.
- Es posible representar documentos inválidos:
- El desarrollador puede escribir mal el nombre del elemento
- El desarrollador puede asociar un atributo con el tipo de elemento erróneo
- El desarrollador puede usar un elemento cerrado cuando lo correcto es un elemento abierto
En el resto del capítulo, aplicaremos ciertas técnicas para resolver estos problemas y convertir nuestra biblioteca en un lenguaje específico del dominio para crear documentos HTML.
Constructores inteligentes (smart constructors)
La primera técnica que aplicaremos es simple pero puede ser muy efectiva. En lugar de exponer la representación de los datos a los usuarios del módulo, podemos usar la lista de exportaciones del módulo para ocultar los constructores de datos Element, Content y Attribute, y exportar únicamente los llamados constructores inteligentes, que construyen datos que se saben correctos.
Aquí tenemos un ejemplo. Primero proporcionamos una función de conveniencia para crear elementos HTML:
1 element :: String -> Array Attribute -> Maybe (Array Content) -> Element
2 element name attribs content = Element
3 { name: name
4 , attribs: attribs
5 , content: content
6 }
A continuación, creamos constructores inteligentes para aquellos elementos HTML que queremos que puedan crear nuestros usuarios, aplicando la función element:
1 a :: Array Attribute -> Array Content -> Element
2 a attribs content = element "a" attribs (Just content)
3
4 p :: Array Attribute -> Array Content -> Element
5 p attribs content = element "p" attribs (Just content)
6
7 img :: Array Attribute -> Element
8 img attribs = element "img" attribs Nothing
Finalmente, actualizamos la lista de exportaciones del módulo para que exporte sólo las funciones que sabemos que construyen estructuras de datos correctas:
1 module Data.DOM.Smart
2 ( Element
3 , Attribute(..)
4 , Content(..)
5
6 , a
7 , p
8 , img
9
10 , render
11 ) where
La lista de exportaciones del módulo se proporciona entre paréntesis inmediatamente después del nombre de módulo. Cada exportación del módulo puede ser de tres tipos:
- Un valor (o función) indicado/a por su nombre.
- Una clase de tipos indicada por el nombre de la clase.
- Un constructor de tipo y cualquier constructor de datos asociado, indicado por el nombre del tipo seguido por una lista entre paréntesis de constructores de datos exportados.
Aquí exportamos el tipo Element, pero no exportamos sus constructores de datos. Si lo hiciésemos, el usuario sería capaz de construir elementos HTML inválidos.
En el caso de los tipos Attribute y Content, seguimos exportando todos los constructores de datos (usando el símbolo .. en la lista de exportación). Aplicaremos la técnica de los constructores inteligentes a estos tipos en breve.
Fíjate en que ya hemos hecho grandes mejoras a nuestra biblioteca:
- Es imposible representar elementos HTML con nombres inválidos (por supuesto, estamos restringidos al conjunto de nombres de elemento proporcionados por la biblioteca).
- Los elementos cerrados no pueden tener contenido por construcción.
Podemos aplicar esta técnica al tipo Content muy fácilmente. Simplemente quitamos los constructores de datos del tipo Content de la lista de exportación y proporcionamos los siguientes constructores inteligentes:
1 text :: String -> Content
2 text = TextContent
3
4 elem :: Element -> Content
5 elem = ElementContent
Apliquemos la misma técnica al tipo Attribute. Primero proporcionamos un constructor inteligente de propósito general para los atributos. Aquí hay un primer intento:
1 attribute :: String -> String -> Attribute
2 attribute key value = Attribute
3 { key: key
4 , value: value
5 }
6
7 infix 4 attribute as :=
Esta representación sufre del mismo problema que el tipo Element original; es posible representar atributos que no existen o cuyos nombres se han escrito mal. Para resolver este problema, podemos crear un newtype que representa nombres de atributo:
1 newtype AttributeKey = AttributeKey String
Con eso, podemos modificar nuestro operador como sigue:
1 attribute :: AttributeKey -> String -> Attribute
2 attribute (AttributeKey key) value = Attribute
3 { key: key
4 , value: value
5 }
Si no exportamos el constructor de datos AttributeKey, el usuario no tiene manera de construir valores de tipo AttributeKey que no sea usando las funciones que exportamos explícitamente. Aquí hay algunos ejemplos:
1 href :: AttributeKey
2 href = AttributeKey "href"
3
4 _class :: AttributeKey
5 _class = AttributeKey "class"
6
7 src :: AttributeKey
8 src = AttributeKey "src"
9
10 width :: AttributeKey
11 width = AttributeKey "width"
12
13 height :: AttributeKey
14 height = AttributeKey "height"
Aquí tenemos la lista final de exportaciones de nuestro nuevo módulo. Date cuenta de que ya no exportamos ningún constructor de datos directamente:
1 module Data.DOM.Smart
2 ( Element
3 , Attribute
4 , Content
5 , AttributeKey
6
7 , a
8 , p
9 , img
10
11 , href
12 , _class
13 , src
14 , width
15 , height
16
17 , attribute, (:=)
18 , text
19 , elem
20
21 , render
22 ) where
Si probamos este nuevo módulo en PSCi, podemos ver grandes mejoras en la concisión del código de usuario:
1 $ pulp repl
2
3 > import Prelude
4 > import Data.DOM.Smart
5 > import Control.Monad.Eff.Console
6 > log $ render $ p [ _class := "main" ] [ text "Hello World!" ]
7
8 <p class="main">Hello World!</p>
9 unit
Fíjate sin embargo en que no tuvimos que hacer cambios a la función render, ya que la representación de los datos subyacentes no ha cambiado. Este es uno de los beneficios de la aproximación de los constructores inteligentes: nos permite separar la representación interna de los datos de un módulo y la representación percibida por los usuarios de su API externa.
Tipos fantasma (phantom types)
Para motivar la siguiente técnica, considera el siguiente código:
1 > log $ render $ img
2 [ src := "cat.jpg"
3 , width := "foo"
4 , height := "bar"
5 ]
6
7 <img src="cat.jpg" width="foo" height="bar" />
8 unit
El problema aquí es que hemos proporcionado valores cadena para los atributos width y height, donde se esperaban valores numéricos en unidades de píxeles o porcentajes.
Para resolver este problema, podemos introducir un argumento de tipo fantasma a nuestro tipo AttributeKey:
1 newtype AttributeKey a = AttributeKey String
La variable de tipo a se llama tipo fantasma porque no hay valores de tipo a involucrados en la parte derecha de la definición. El tipo a sólo existe para proporcionar más información en tiempo de compilación. Cualquier valor de tipo AttributeKey a es simplemente una cadena en tiempo de ejecución, pero en tiempo de compilación, el tipo del valor nos dice el tipo deseado para los valores asociados con esta clave.
Podemos modificar el tipo de nuestra función attribute para que tome en consideración la nueva forma de AttributeKey:
1 attribute :: forall a. IsValue a => AttributeKey a -> a -> Attribute
2 attribute (AttributeKey key) value = Attribute
3 { key: key
4 , value: toValue value
5 }
Aquí, el argumento de tipo fantasma a se usa para asegurarnos de que la clave y valor del atributo tienen tipos compatibles. Como el usuario no puede crear valores de tipo AttributeKey a directamente (sólo mediante las constantes que proporcionamos en la biblioteca), todos los atributos serán correctos por construcción.
Fíjate en que la restricción IsValue nos asegura que asociemos el tipo de valor que asociemos a una clave, sus valores se pueden convertir en cadenas para mostrarse en el HTML generado. La clase de tipos IsValue se define como sigue:
1 class IsValue a where
2 toValue :: a -> String
Proporcionamos instancias de la clase de tipos para los tipos String e Int:
1 instance stringIsValue :: IsValue String where
2 toValue = id
3
4 instance intIsValue :: IsValue Int where
5 toValue = show
Tenemos también que actualizar nuestras constantes AttributeKey de manera que sus tipos reflejen el nuevo parámetro de tipo:
1 href :: AttributeKey String
2 href = AttributeKey "href"
3
4 _class :: AttributeKey String
5 _class = AttributeKey "class"
6
7 src :: AttributeKey String
8 src = AttributeKey "src"
9
10 width :: AttributeKey Int
11 width = AttributeKey "width"
12
13 height :: AttributeKey Int
14 height = AttributeKey "height"
Ahora nos encontramos con que es imposible representar estos documentos HTML inválidos, y que estamos obligados a usar números para representar los atributos width y height:
1 > import Prelude
2 > import Data.DOM.Phantom
3 > import Control.Monad.Eff.Console
4
5 > :paste
6 … log $ render $ img
7 … [ src := "cat.jpg"
8 … , width := 100
9 … , height := 200
10 … ]
11 … ^D
12
13 <img src="cat.jpg" width="100" height="200" />
14 unit
La mónada libre (free monad)
En nuestro conjunto final de modificaciones a nuestra API, usaremos un constructor llamado la mónada libre para convertir nuestro tipo Content en una mónada, habilitando la notación do. Esto nos permitirá estructurar nuestro documento HTML en una forma en que el anidamiento de elementos se vuelve más claro; en lugar de esto:
1 p [ _class := "main" ]
2 [ elem $ img
3 [ src := "cat.jpg"
4 , width := 100
5 , height := 200
6 ]
7 , text "A cat"
8 ]
podremos escribir esto:
1 p [ _class := "main" ] $ do
2 elem $ img
3 [ src := "cat.jpg"
4 , width := 100
5 , height := 200
6 ]
7 text "A cat"
Sin embargo, la notación do no es el único beneficio de una mónada libre. La mónada libre nos permite separar la representación de nuestras acciones monádicas de su interpretación, e incluso soportar múltiples interpretaciones de las mismas acciones.
La mónada Free se define en la biblioteca purescript-free en el módulo Control.Monad.Free. Podemos averiguar alguna información básica sobre ella en PSCi como sigue:
1 > import Control.Monad.Free
2
3 > :kind Free
4 (Type -> Type) -> Type -> Type
La familia de Free indica que toma un constructor de tipo como argumento y devuelve otro constructor de tipo. De hecho, ¡la mónada Free se puede usar para convertir cualquier Functor en una Monad!
Comenzamos definiendo la representación de nuestras acciones monádicas. Para hacer esto necesitamos crear un Functor con un constructor de datos para cada acción monádica que deseamos soportar. En nuestro caso, nuestras dos acciones monádicas serán elem y text. De hecho podemos simplemente modificar nuestro tipo Content como sigue:
1 data ContentF a
2 = TextContent String a
3 | ElementContent Element a
4
5 instance functorContentF :: Functor ContentF where
6 map f (TextContent s x) = TextContent s (f x)
7 map f (ElementContent e x) = ElementContent e (f x)
Aquí el constructor de tipo ContentF se parece a nuestro viejo tipo de datos Content; sin embargo, ahora toma un argumento de tipo a, y cada constructor de datos se ha modificado para que tome un valor de tipo a como argumento adicional. La instancia Functor simplemente aplica la función f al valor de tipo a en cada constructor de datos.
Con eso, podemos definir nuestra nueva mónada Content como un sinónimo de tipo para la mónada Free, que construimos usando nuestro constructor de tipo ContentF como primer argumento de tipo:
1 type Content = Free ContentF
En lugar de un sinónimo de tipo podemos usar un newtype para evitar exponer la representación interna de nuestra biblioteca a los usuarios. Ocultando el constructor de datos Content restringimos a nuestros usuarios a que usen únicamente las acciones monádicas que suministramos.
Como ContentF es un Functor, obtenemos automáticamente una instancia Monad para Free ContentF.
Tenemos que modificar nuestro tipo de datos Element ligeramente para tomar en cuenta el nuevo argumento de tipo de Content. Simplemente requeriremos que el tipo de retorno de nuestros cálculos monádicos sea Unit:
1 newtype Element = Element
2 { name :: String
3 , attribs :: Array Attribute
4 , content :: Maybe (Content Unit)
5 }
Además tenemos que modificar nuestras funciones elem y text, que se convertirán en nuestras nuevas acciones monádicas para la mónada Content. Para hacer esto podemos usar la función liftF suministrada por el módulo Control.Monad.Free. Aquí está su tipo:
1 liftF :: forall f a. f a -> Free f a
liftF nos permite construir una acción en nuestra mónada libre a partir de un valor de tipo f a para algún tipo a. En nuestro caso, podemos simplemente usar los constructores de datos de nuestro constructor de tipo ContentF directamente:
1 text :: String -> Content Unit
2 text s = liftF $ TextContent s unit
3
4 elem :: Element -> Content Unit
5 elem e = liftF $ ElementContent e unit
Hay que hacer otras modificaciones rutinarias, pero los cambios interesantes están en la función render, donde tenemos que interpretar nuestra mónada libre.
Interpretando la mónada
El módulo Control.Monad.Free proporciona funciones para interpretar un cálculo en una mónada libre:
1 runFree
2 :: forall f a
3 . Functor f
4 => (f (Free f a) -> Free f a)
5 -> Free f a
6 -> a
7
8 runFreeM
9 :: forall f m a
10 . (Functor f, MonadRec m)
11 => (f (Free f a) -> m (Free f a))
12 -> Free f a
13 -> m a
La función runFree se usa para calcular un resultado puro. La función runFreeM nos permite usar una mónada para interpretar las acciones de nuestra mónada libre.
Nota: Técnicamente, estamos restringidos a usar monadas m que satisfagan la restricción más fuerte MonadRec. En la práctica, esto significa que no necesitamos preocuparnos por los desbordamientos de pila, ya que m soporta recursividad final mónadica segura.
Primero tenemos que elegir una mónada en la que podamos interpretar nuestras acciones. Usaremos la mónada Writer String para acumular una cadena HTML como resultado.
Nuestro nuevo método render comienza delegando en una función auxiliar renderElement y usando execWriter para ejecutar nuestro cálculo en la mónada Writer:
1 render :: Element -> String
2 render = execWriter <<< renderElement
renderElement se define en un bloque where:
1 where
2 renderElement :: Element -> Writer String Unit
3 renderElement (Element e) = do
La definición de renderElement es simple, usa la acción tell de la mónada Writer para acumular varias cadenas pequeñas:
1 tell "<"
2 tell e.name
3 for_ e.attribs $ \x -> do
4 tell " "
5 renderAttribute x
6 renderContent e.content
A continuación definimos la función renderAttribute que es igualmente simple:
1 where
2 renderAttribute :: Attribute -> Writer String Unit
3 renderAttribute (Attribute x) = do
4 tell x.key
5 tell "=\""
6 tell x.value
7 tell "\""
La función renderContent es más interesante. Aquí usamos la función runFreeM para interpretar el cálculo dentro de la mónada libre, delegando en una función auxiliar renderContentItem:
1 renderContent :: Maybe (Content Unit) -> Writer String Unit
2 renderContent Nothing = tell " />"
3 renderContent (Just content) = do
4 tell ">"
5 runFreeM renderContentItem content
6 tell "</"
7 tell e.name
8 tell ">"
El tipo de renderContentItem se puede deducir de la firma de tipo de runFreeM. El funtor f es nuestro constructor de tipo ContentF, y la mónada m es la mónada en la que estamos interpretando el cálculo, a saber, Writer String. Esto nos da la siguiente firma de tipo para renderContentItem:
1 renderContentItem :: ContentF (Content Unit) -> Writer String (Content Uni\
2 t)
Podemos implementar esta función simplemente mediante ajuste de patrones sobre los dos constructores de datos de ContentF:
1 renderContentItem (TextContent s rest) = do
2 tell s
3 pure rest
4 renderContentItem (ElementContent e rest) = do
5 renderElement e
6 pure rest
En cada caso, la expresión rest tiene el tipo Content Unit, y representa el resto del cálculo interpretado. Podemos completar cada caso devolviendo la acción rest.
¡Eso es todo! Podemos probar nuestra nueva API monádica en PSCi como sigue:
1 > import Prelude
2 > import Data.DOM.Free
3 > import Control.Monad.Eff.Console
4
5 > :paste
6 … log $ render $ p [] $ do
7 … elem $ img [ src := "cat.jpg" ]
8 … text "A cat"
9 … ^D
10
11 <p><img src="cat.jpg" />A cat</p>
12 unit
Extendiendo el lenguaje
Una mónada en la que todas las acciones devuelven algo de tipo Unit no es particularmente interesante. De hecho, aparte de una sintaxis probablemente más bonita, nuestra mónada no añade funcionalidad extra con respecto a un Monoid.
Ilustremos la potencia de la construcción de mónada libre extendiendo nuestro lenguaje con una nueva acción monádica que devuelve un resultado no trivial.
Supongamos que queremos generar documentos HTML que contienen hipervínculos a diferentes secciones del documento usando anclas (anchors). Podemos conseguir esto actualmente generando nombres de ancla a mano e incluyéndolos al menos dos veces en el documento: una vez en la definición de la propia ancla, y otra en cada hipervínculo. Sin embargo, este enfoque tiene algunos problemas básicos:
- El desarrollador puede equivocarse generando nombres de ancla únicos.
- El desarrollador puede equivocarse al escribir una o más instancias del nombre del ancla.
Queriendo proteger al desarrollador de sus propios errores, podemos introducir un nuevo tipo que representa nombres de ancla y proporcionar una acción monádica para generar nuevos nombres únicos.
El primer paso es añadir un nuevo tipo para los nombres:
1 newtype Name = Name String
2
3 runName :: Name -> String
4 runName (Name n) = n
De nuevo, definimos esto como un newtype en torno a String, pero debemos ser cuidadosos de no exportar el constructor de datos en la lista de exportaciones del módulo.
A continuación definimos una instancia de la clase de tipos IsValue para nuestro nuevo tipo, de manera que seamos capaces de usar nombres en valores de atributo:
1 instance nameIsValue :: IsValue Name where
2 toValue (Name n) = n
Definimos también un nuevo tipo de datos para hipervínculos que pueden aparecer en elementos a como sigue:
1 data Href
2 = URLHref String
3 | AnchorHref Name
4
5 instance hrefIsValue :: IsValue Href where
6 toValue (URLHref url) = url
7 toValue (AnchorHref (Name nm)) = "#" <> nm
Con este nuevo tipo, podemos modificar el tipo de valor del atributo href, forzando a nuestros usuarios a usar nuestro nuevo tipo Href. Podemos también crear un nuevo atributo name que se puede usar para convertir un elemento en un ancla:
1 href :: AttributeKey Href
2 href = AttributeKey "href"
3
4 name :: AttributeKey Name
5 name = AttributeKey "name"
El problema restante es que nuestros usuarios no tienen actualmente manera de generar nombres nuevos. Podemos proporcionar esta funcionalidad en nuestra mónada Content. Primero necesitamos añadir un nuevo constructor de datos a nuestro constructor de tipo ContentF:
1 data ContentF a
2 = TextContent String a
3 | ElementContent Element a
4 | NewName (Name -> a)
El constructor de datos NewName corresponde a una acción que devuelve un valor de tipo Name. Fíjate en que en lugar de requerir un Name como argumento de constructor de datos, requerimos que el usuario proporcione una función de tipo Name -> a. Recordando que el tipo a representa el resto del cálculo, podemos ver que esta función proporciona una manera de continuar el cálculo después de que un valor de tipo Name se haya devuelto.
Necesitamos también actualizar la instancia Functor de ContentF para que tenga cuenta el nuevo constructor de datos como sigue:
1 instance functorContentF :: Functor ContentF where
2 map f (TextContent s x) = TextContent s (f x)
3 map f (ElementContent e x) = ElementContent e (f x)
4 map f (NewName k) = NewName (f <<< k)
Podemos ahora construir nuestra nueva acción mediante la función liftF como antes:
1 newName :: Content Name
2 newName = liftF $ NewName id
Date cuenta de que proporcionamos la función id como nuestra continuación, lo que significa que devolvemos el resultado de tipo Name sin cambiar.
Finalmente necesitamos actualizar nuestra función de interpretación para interpretar la nueva acción. Previamente hemos usado la mónada Writer String para interpretar nuestros cálculos, pero esa mónada no tiene la capacidad de generar nuevos nombres, de manera que cambiamos a otra cosa. El transformador de mónada WriterT se puede usar con la mónada State para combinar los efectos que necesitamos. Definimos nuestra mónada de interpretación como un sinónimo de tipo para mantener nuestras firmas de tipo cortas:
1 type Interp = WriterT String (State Int)
Aquí, el estado de tipo Int actuará como un contador incremental usado para generar nombres únicos.
Ya que las mónadas Writer y WriterT usan los mismos miembros de clase de tipos para abstraer sus acciones, no necesitamos cambiar ninguna acción; sólo tenemos que cambiar todas las referencias a Writer String por Interp. Sin embargo tenemos que modificar el gestor usado para ejecutar nuestro cálculo. En lugar de usar execWriter, tenemos ahora que usar evalState y execWriter:
1 render :: Element -> String
2 render e = evalState (execWriterT (renderElement e)) 0
También necesitamos añadir un nuevo caso a renderContentItem para interpretar el constructor de datos NewName:
1 renderContentItem (NewName k) = do
2 n <- get
3 let fresh = Name $ "name" <> show n
4 put $ n + 1
5 pure (k fresh)
Aquí se nos da una continuación k de tipo Name -> Content a, y necesitamos construir una interpretación de tipo Content a. Nuestra interpretación es simple: usamos get para leer el estado, usamos ese estado para generar un nombre único, y usamos put para incrementar el estado. Finalmente pasamos nuestro nuevo nombre a la continuación para completar el cálculo.
Con eso, podemos probar nuestra nueva funcionalidad en PSCi, generando un nombre único dentro de la mónada Content y usándolo tanto como nombre de elemento como destino de un hipervínculo:
1 > import Prelude
2 > import Data.DOM.Name
3 > import Control.Monad.Eff.Console
4
5 > :paste
6 … render $ p [ ] $ do
7 … top <- newName
8 … elem $ a [ name := top ] $
9 … text "Top"
10 … elem $ a [ href := AnchorHref top ] $
11 … text "Back to top"
12 … ^D
13
14 <p><a name="name0">Top</a><a href="#name0">Back to top</a></p>
15 unit
Puedes verificar que múltiples llamadas a newName resultan de hecho en nombres únicos.
Conclusión
En este capítulo hemos desarrollado un lenguaje específico del dominio para crear documentos HTML, mejorando incrementalmente una implementación ingenua usando algunas técnicas estándar:
- Hemos usado constructores inteligentes para esconder detalles de nuestra representación de datos, permitiendo únicamente al usuario crear documentos que sean correctos por construcción.
- Hemos usado un operador binario infijo definido por el usuario para mejorar la sintaxis del lenguaje.
- Hemos usado tipos fantasma para codificar información adicional en los tipos de nuestros datos, evitando que el usuario proporcione valores de tipo erróneo.
- Hemos usado la mónada libre para convertir nuestra representación array del contenido en una representación monádica que soporta notación do. Hemos extendido esta representación para soportar una nueva acción monádica, y hemos interpretado los cálculos monádicos usando transformadores de mónada estándar.
Estas técnicas aprovechan el sistema de tipos y módulos de JavaScript, bien para evitar que el usuario cometa errores, bien para mejorar la sintaxis del lenguaje específico del dominio.
La implementación de lenguajes específicos del dominio en lenguajes de programación funcional es un área de investigación activa, pero con suerte esto proporciona una introducción útil a algunas técnicas simples e ilustra la potencia de trabajar en un lenguaje con tipos expresivos.