¿Es mejor usar la mónada de error con validación en sus funciones monádicas, o implementar su propia mónada con validación directamente en su enlace?

9

Me pregunto qué es mejor en cuanto al diseño para la usabilidad / mantenibilidad, y qué es mejor en lo que respecta a la comunidad.

Dado el modelo de datos:

type Name = String

data Amount = Out | Some | Enough | Plenty deriving (Show, Eq)
data Container = Container Name deriving (Show, Eq)
data Category = Category Name deriving (Show, Eq)
data Store = Store Name [Category] deriving (Show, Eq)
data Item = Item Name Container Category Amount Store deriving Show
instance Eq (Item) where
  (==) i1 i2 = (getItemName i1) == (getItemName i2)

data User = User Name [Container] [Category] [Store] [Item] deriving Show
instance Eq (User) where
  (==) u1 u2 = (getName u1) == (getName u2)

Puedo implementar funciones monádicas para transformar el usuario, por ejemplo, agregando elementos o tiendas, etc., pero puedo terminar con un usuario no válido, por lo que esas funciones monádicas tendrían que validar el usuario que obtienen o crean.

Entonces, ¿debería simplemente:

  • envolverlo en una mónada de error y hacer que las funciones monádicas ejecuten la validación
  • envuélvala en una mónada de error y haga que el consumidor vincule una función de validación monádica en la secuencia que arroja la respuesta de error apropiada (para que pueda elegir no validar y transportar un objeto de usuario no válido)
  • en realidad lo construyo en una instancia de enlace en el usuario creando efectivamente mi propio tipo de mónada de error que ejecuta la validación con cada enlace automáticamente

Puedo ver aspectos positivos y negativos de cada uno de los 3 enfoques, pero quiero saber qué hace más comúnmente la comunidad para este escenario.

Entonces, en términos de código, como la opción 1:

addStore s (User n1 c1 c2 s1 i1) = validate $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ someUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"]

opcion 2:

addStore s (User n1 c1 c2 s1 i1) = Right $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ Right someUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"] >>= validate
-- in this choice, the validation could be pushed off to last possible moment (like inside updateUsersTable before db gets updated)

opción 3:

data ValidUser u = ValidUser u | InvalidUser u
instance Monad ValidUser where
    (>>=) (ValidUser u) f = case return u of (ValidUser x) -> return f x; (InvalidUser y) -> return y
    (>>=) (InvalidUser u) f = InvalidUser u
    return u = validate u

addStore (Store s, User u, ValidUser vu) => s -> u -> vu
addStore s (User n1 c1 c2 s1 i1) = return $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ someValidUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"]
Jimmy Hoffa
fuente

Respuestas:

5

Puño, me preguntaría: ¿Tiene un Usererror de código no válido o una situación que normalmente puede ocurrir (por ejemplo, alguien que ingresa una entrada incorrecta a su aplicación)? Si es un error, trataría de asegurarme de que nunca suceda (como usar constructores inteligentes o crear tipos más sofisticados).

Si es un escenario válido, entonces es apropiado algún procesamiento de error durante el tiempo de ejecución. Luego preguntaría: ¿Qué significa realmente para mí que a noUser sea válido ?

  1. ¿Significa que un inválido Userpuede hacer que falle un código? ¿Se basan partes de su código en el hecho de que a Usersiempre es válido?
  2. ¿O simplemente significa que es una inconsistencia que debe corregirse más tarde, pero que no rompe nada durante el cálculo?

Si es 1., definitivamente elegiría algún tipo de error mónada (ya sea estándar o propio), de lo contrario perderá las garantías de que su código funciona correctamente.

Crear su propia mónada o usar una pila de transformadores de mónada es otro problema, tal vez esto sea útil: ¿Alguien ha encontrado alguna vez un Transformador de mónada en la naturaleza? .


Actualización: mirando sus opciones ampliadas:

  1. Parece el mejor camino a seguir. Tal vez, para estar realmente seguro, prefiero ocultar el constructor Usery, en su lugar, exportar solo algunas funciones que no permiten crear una instancia no válida. De esta manera, se asegurará de que cada vez que ocurra se maneje correctamente. Por ejemplo, una función genérica para crear un Userpodría ser algo como

    user :: ... -> Either YourErrorType User
    -- more generic:
    user :: (MonadError YourErrorType m) ... -> m User
    -- Or if you actually don't need to differentiate errors:
    user :: ... -> Maybe User
    -- or more generic:
    user :: (MonadPlus m) ... -> m User
    -- etc.
    

    Muchas bibliotecas toman un enfoque similar, por ejemplo Map, Setu Seqocultan la implementación subyacente para que no sea posible crear una estructura que no obedezca a sus invariantes.

  2. Si pospone la validación hasta el final y la usa en Right ...todas partes, ya no necesita una mónada. Puede hacer cálculos puros y resolver cualquier posible error al final. En mi humilde opinión, este enfoque es muy arriesgado, ya que un valor de usuario no válido puede llevar a tener datos no válidos en otro lugar, porque no detuvo el cálculo lo suficientemente pronto. Y, si sucede que algún otro método actualiza al usuario para que sea válido nuevamente, terminará teniendo datos no válidos en algún lugar y sin siquiera saberlo.

  3. Hay varios problemas aquí.

    • Lo más importante es que una mónada debe aceptar cualquier parámetro de tipo, no solo User. Por validatelo tanto, debería tener un tipo de letra u -> ValidUser usin ninguna restricción u. Por lo tanto, no es posible escribir una mónada que valide las entradas de return, porque returndebe ser completamente polimórfica.
    • A continuación, lo que no entiendo es que coinciden case return u ofen la definición de >>=. El punto principal de ValidUserdebe ser distinguir valores válidos e inválidos, por lo que la mónada debe asegurarse de que esto sea siempre cierto. Entonces podría ser simplemente

      (>>=) (ValidUser u) f = f u
      (>>=) (InvalidUser u) f = InvalidUser u
      

    Y esto ya se parece mucho Either.

En general, usaría una mónada personalizada solo si

  • No hay mónadas existentes que le brinden la funcionalidad que necesita. Las mónadas existentes generalmente tienen muchas funciones de soporte y, lo que es más importante, tienen transformadores de mónadas para que pueda componerlas en pilas de mónadas.
  • O si necesita una mónada que sea demasiado compleja para describirla como una pila de mónadas.
Petr Pudlák
fuente
¡Tus dos últimos puntos son invaluables y no pensé en ellos! Definitivamente la sabiduría que estaba buscando, gracias por compartir tus pensamientos, ¡definitivamente iré con el # 1!
Jimmy Hoffa
Atasé todo el módulo anoche y estabas en lo cierto. Introduje mi método de validación en un pequeño número de combinadores de teclas que tenía haciendo todas las actualizaciones del modelo y en realidad tiene mucho más sentido como este. Realmente iba a ir después del # 3 y ahora veo cuán ... inflexible habría sido ese enfoque, ¡así que muchas gracias por aclararme!
Jimmy Hoffa