Melhor usar a mônada de erro com validação em suas funções monádicas ou implementar sua própria mônada com validação diretamente em seu vínculo?

9

Eu estou imaginando o que é melhor em termos de design para usabilidade / manutenção e o que é melhor em relação à comunidade.

Dado o modelo de dados:

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)

Posso implementar funções monádicas para transformar o Usuário, por exemplo, adicionando itens ou lojas, etc., mas posso acabar com um usuário inválido, para que essas funções monádicas precisem validar o usuário que obtêm e / ou criam.

Então, devo apenas:

  • envolva-o em uma mônada de erro e faça com que as funções monádicas executem a validação
  • envolva-o em uma mônada de erro e faça o consumidor ligar uma função de validação monádica na sequência que lança a resposta de erro apropriada (para que eles possam optar por não validar e transportar um objeto de usuário inválido)
  • na verdade, crie-o em uma instância de ligação no Usuário, criando efetivamente meu próprio tipo de mônada de erro que executa a validação com cada ligação automaticamente

Eu posso ver pontos positivos e negativos de cada uma das três abordagens, mas quero saber o que é mais comumente feito para esse cenário pela comunidade.

Portanto, em termos de código, algo como a opção 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"]

opção 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)

opção 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
fonte

Respostas:

5

Primeiro, eu me perguntava: está com um Usererro de código inválido ou uma situação que normalmente pode ocorrer (por exemplo, alguém inserindo uma entrada errada no seu aplicativo). Se for um erro, tentarei garantir que isso nunca aconteça (como usar construtores inteligentes ou criar tipos mais sofisticados).

Se for um cenário válido, algum processamento de erro durante o tempo de execução é apropriado. Então eu perguntava: o que realmente significa para mim que a Useré inválido ?

  1. Isso significa que um inválido Userpode fazer com que algum código falhe? Partes do seu código dependem do fato de que a Useré sempre válido?
  2. Ou significa apenas que é uma inconsistência que precisa ser corrigida mais tarde, mas não quebra nada durante o cálculo?

Se for 1., eu definitivamente optaria por algum tipo de erro de mônada (padrão ou seu), caso contrário você perderá garantias de que seu código está funcionando corretamente.

Criar sua própria mônada ou usar uma pilha de transformadores de mônada é outra questão, talvez isso seja útil: alguém já encontrou um transformador de mônada em estado selvagem? .


Atualização: examinando suas opções expandidas:

  1. Parece ser o melhor caminho a percorrer. Talvez, para ser realmente seguro, prefira ocultar o construtor Usere exportar apenas algumas funções que não permitem criar uma instância inválida. Dessa forma, você terá certeza de que, sempre que isso acontecer, será tratado adequadamente. Por exemplo, uma função genérica para criar um Userpoderia 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.
    

    Muitas bibliotecas adotam uma abordagem semelhante, por exemplo Map, Setou Seqocultam a implementação subjacente para que não seja possível criar uma estrutura que não obedeça a seus invariantes.

  2. Se você adiar a validação até o fim e usá-la em Right ...qualquer lugar, não precisará mais de uma mônada. Você pode fazer cálculos puros e resolver possíveis erros no final. IMHO essa abordagem é muito arriscada, pois um valor de usuário inválido pode levar a dados inválidos em outro lugar, porque você não interrompeu o cálculo em breve. E, se algum outro método atualizar o usuário para que ele seja válido novamente, você terá dados inválidos em algum lugar e nem saberá.

  3. Existem vários problemas aqui.

    • O mais importante é que uma mônada deve aceitar qualquer parâmetro de tipo, não apenas User. Portanto, você validateteria que ter um tipo u -> ValidUser usem nenhuma restrição u. Portanto, não é possível escrever uma mônada que valide entradas de return, porque returndeve ser totalmente polimórfica.
    • Em seguida, o que não entendo é que você corresponde case return u ofna definição de >>=. O ponto principal de ValidUserdeve ser distinguir valores válidos e inválidos e, portanto, a mônada deve garantir que isso seja sempre verdadeiro. Então poderia ser simplesmente

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

    E isso já se parece muito Either.

Geralmente, eu usaria uma mônada personalizada apenas se

  • Não há mônadas existentes que ofereçam a funcionalidade necessária. As mônadas existentes geralmente têm muitas funções de suporte e, mais importante, têm transformadores de mônadas, para que você possa compô-las em pilhas de mônadas.
  • Ou se você precisar de uma mônada que seja muito complexa para ser descrita como uma pilha de mônadas.
Petr Pudlák
fonte
Seus últimos dois pontos são inestimáveis ​​e eu não pensei neles! Definitivamente a sabedoria que eu estava procurando, obrigado por compartilhar seus pensamentos, eu definitivamente irei com o # 1!
Jimmy Hoffa
Apenas amarrei o módulo inteiro ontem à noite e você estava completamente certo. Coloquei meu método de validação em um pequeno número de combinadores de chaves que eu tinha feito todas as atualizações de modelo e, na verdade, faz muito mais sentido assim. Eu realmente iria atrás do # 3 e agora vejo como ... essa abordagem seria inflexível, então muito obrigado por me esclarecer!
Jimmy Hoffa