haskell - quiere - ¿Qué es la Mónada RWS y cuándo se utiliza?
neo monadología (2)
El estado es un concepto muy generalizado y, por lo tanto, puede usarse para muchas cosas. Se puede pensar en Reader y Writer en una forma especializada de Estado con algunas restricciones, solo se puede leer de un lector y solo se puede escribir a un escritor. Al utilizar esta forma de estado especializada, puede ser más explícito sobre lo que está tratando de lograr o cuáles son exactamente las intenciones.
Otra analogía podría ser usar el mapa / diccionario para modelar cualquier cosa (objetos, datos, controladores de eventos, etc.), pero usar una forma más especializada de mapa / diccionario hace que las cosas sean más explícitas.
Me encontré con la RWS Monad y su MonadTransformer mientras buscaba algo en la biblioteca mtl. No hay documentación real allí, y me preguntaba qué es esto y dónde se usa.
Llegué a la conclusión de que RWS es un acrónimo de Reader, Writer, State y es una pila de esos tres transformadores de mónada. Lo que no puedo entender es por qué esto es mejor que el estado por sí mismo.
La razón más práctica para esto es para la capacidad de prueba y para firmas de tipo más precisas.
La fortaleza clave de haskell es qué tan bien puede especificar qué hace una función a través del sistema de tipos. Compara el tipo c # / java:
public int CSharpFunction(int param) { ...
con un haskell uno:
someFunction :: Int -> Int
El haskell uno no solo nos dice los tipos necesarios para los parámetros y el tipo de retorno, sino también a qué puede afectar la función. Por ejemplo, no puede hacer ningún IO, ni puede leer ni cambiar ningún estado global, ni acceder a ningún dato de configuración estático. Tampoco puede ser cierto para la función c #, pero no podemos decirlo.
Esta es una gran ayuda con las pruebas. Sabemos que lo único que necesitamos probar con someFunction
es si obtiene los resultados esperados para algunas entradas de muestra. No se requiere ninguna configuración posible, y la función nunca dará un resultado diferente para la misma entrada. Esto hace que las pruebas sean bastante simples con funciones puras.
Sin embargo, a menudo una función no puede ser pura. Por ejemplo, puede necesitar acceder a cierta información global solo para leer. Podríamos simplemente agregar otro parámetro a la función:
readerFunc :: GlobalConfig -> Int -> Int
Pero a menudo es más fácil usar una mónada, ya que son más fáciles de componer:
readerFunc2 :: Int -> Reader GlobalConfig Int
Probar esto es casi tan fácil como probar una función pura. Solo necesitamos probar varias permutaciones del valor Int de entrada y el valor de configuración del lector GlobalConfig.
Una función puede necesitar escribir valores (por ejemplo, para el registro). Esto también se puede hacer con una mónada:
writerFunc :: Int -> Writer String Int
Una vez más, probar esto es casi tan fácil como para una función pura. Solo probamos si para una entrada Int
determinada, se devuelve el Int
apropiado, así como la String
escritura final correcta.
Otra función puede necesitar leer y cambiar el estado:
stateFunc :: Int -> State GlobalState Int
Aunque esto es más difícil de probar. No solo tenemos que verificar la salida utilizando varios Ints de entrada y GlobalStates iniciales, sino que también debemos comprobar si el GlobalState final es el valor correcto.
Consideremos ahora una función que:
- Necesita leer de un tipo de datos ProgramConfig
- Escribir valores en una cadena para el registro
- Utilice y modifique un valor
ProgramState
.
Podríamos hacer algo como esto:
data ProgramData = ProgramData { pState :: ProgramState, pConfig :: ProgramConfig, pLogs :: String }
complexFunction :: Int -> State ProgramData Int
Sin embargo, ese tipo no es muy preciso. Implica que se puede cambiar el ProgramConfig, que no lo hará. También implica que la función puede usar el valor pLogs, que no lo hará. Además, probarlo es más complejo, como teóricamente, la función podría cambiar accidentalmente la configuración del programa, o usar el valor actual de pLogs en sus cálculos.
Esto se puede mejorar enormemente con esto:
betterFunction :: Int -> RWS ProgramConfig String ProgramState Int
Este tipo es muy preciso. Sabemos que el ProgramConfig solo se lee desde. Sabemos que la String
sólo se cambia. La única parte que requiere lectura y escritura es el ProgramState
, como se esperaba. Esto es más fácil de probar, ya que solo necesitamos probar diferentes combinaciones de ProgramConfig, ProgramState e Int
para la entrada, y verificar la salida Int
, String
y ProgramState
para la salida. Sabemos que no podemos cambiar accidentalmente la configuración del programa, o usar el valor actual del registro del programa en nuestros cálculos.
El sistema de tipo haskell está ahí para ayudarnos, deberíamos darle toda la información que podamos para que pueda detectar errores antes de que lo hagamos.
(Tenga en cuenta que cada tipo de haskell en esta respuesta podría ser el equivalente al tipo c # en la parte superior, dependiendo de cómo se implementa realmente la función c #).