scala configuration monads reader-monad

Datos de configuración en Scala-¿Debo usar la mónada Reader?



configuration monads (1)

¿Cómo creo un objeto configurable correctamente funcional en Scala? He visto el video de Tony Morris en la mónada Reader y todavía no puedo conectar los puntos.

Tengo una lista codificada de objetos de Client :

class Client(name : String, age : Int){ /* etc */} object Client{ //Horrible! val clients = List(Client("Bob", 20), Client("Cindy", 30)) }

Quiero que Client.clients se determine en tiempo de ejecución, con la flexibilidad de leerlo desde un archivo de propiedades o desde una base de datos. En el mundo de Java definiría una interfaz, implementaría los dos tipos de fuente y usaría DI para asignar una variable de clase:

trait ConfigSource { def clients : List[Client] } object ConfigFileSource extends ConfigSource { override def clients = buildClientsFromProperties(Properties("clients.properties")) //...etc, read properties files } object DatabaseSource extends ConfigSource { /* etc */ } object Client { @Resource("configuration_source") private var config : ConfigSource = _ //Inject it at runtime val clients = config.clients }

Esto me parece una solución bastante limpia (no mucho código, intención clara), pero esa var salta (OTOH, no me parece realmente problemático, ya que sé que se inyectará una vez y sólo una vez).

¿Cómo sería la mónada Reader en esta situación y, explícamelo como si tuviera 5, cuáles son sus ventajas?


Comencemos con una diferencia simple y superficial entre su enfoque y el enfoque de Reader , que es que ya no necesita aferrarse a config ningún lugar. Digamos que define el siguiente sinónimo de tipo vagamente inteligente:

type Configured[A] = ConfigSource => A

Ahora, si alguna vez necesito un ConfigSource para alguna función, digamos una función que ConfigSource al cliente n ''th en la lista, puedo declarar esa función como "configurada":

def nthClient(n: Int): Configured[Client] = { config => config.clients(n) }

¡Así que básicamente estamos sacando una config de la nada, cada vez que la necesitamos! Huele a inyección de dependencia, ¿verdad? Ahora digamos que queremos las edades del primer, segundo y tercer cliente en la lista (asumiendo que existen):

def ages: Configured[(Int, Int, Int)] = for { a0 <- nthClient(0) a1 <- nthClient(1) a2 <- nthClient(2) } yield (a0.age, a1.age, a2.age)

Para esto, por supuesto, necesita una definición adecuada de map y flatMap . No voy a entrar en eso aquí, pero simplemente diré que Scalaz (o la charla de Nescala de Rúnar , o la de Tony''s que ya has visto) te da todo lo que necesitas.

El punto importante aquí es que la dependencia de ConfigSource y su llamada inyección están en su mayoría ocultas. La única "pista" que podemos ver aquí es que las ages son del tipo Configured[(Int, Int, Int)] lugar de simplemente (Int, Int, Int) . No necesitábamos hacer referencia explícita a la config ningún lugar.

Por otro lado , esta es la manera en que casi siempre me gusta pensar en las mónadas: ocultan su efecto para no contaminar el flujo de tu código, mientras declaran explícitamente el efecto en la firma de tipo. En otras palabras, no necesita repetirse demasiado: dice "hey, esta función trata con el efecto X " en el tipo de retorno de la función, y no se mete más con ella.

En este ejemplo, por supuesto, el efecto es leer desde algún entorno fijo. Otro efecto monádico con el que podría estar familiarizado incluye el manejo de errores: podemos decir que la Option oculta la lógica del manejo de errores al tiempo que hace que la posibilidad de errores sea explícita en el tipo de su método. O, de manera opuesta a la lectura, la mónada del Writer oculta lo que escribimos y hace explícita su presencia en el sistema de tipos.

Finalmente, al igual que normalmente necesitamos reiniciar un marco DI (en algún lugar fuera de nuestro flujo de control habitual, como en un archivo XML), también necesitamos reiniciar esta curiosa mónada. Seguramente tendremos algún punto de entrada lógico a nuestro código, como por ejemplo:

def run: Configured[Unit] = // ...

Termina siendo bastante simple: dado que Configured[A] es solo un sinónimo de tipo para la función ConfigSource => A , solo podemos aplicar la función a su "entorno":

run(ConfigFileSource) // or run(DatabaseSource)

Ta-da! Por lo tanto, al contrastar con el enfoque DI tradicional al estilo Java, no tenemos ninguna "magia" que ocurra aquí. La única magia, por así decirlo, está encapsulada en la definición de nuestro tipo Configured y la forma en que se comporta como una mónada. Lo que es más importante, el sistema de tipo nos mantiene honestos acerca de qué inyección de dependencia de "reino" está ocurriendo en: cualquier cosa con tipo Configured[...] está en el mundo DI, y cualquier cosa sin él no lo es. Simplemente no obtenemos esto en la DI de la vieja escuela, donde todo es potencialmente manejado por la magia, por lo que no se sabe qué partes de su código son seguras para reutilizar fuera de un marco DI (por ejemplo, dentro de su unidad) pruebas, o en algún otro proyecto por completo).

actualización: escribí una publicación de blog que explica Reader en mayor detalle.