Functional Programming – How to Use Error Monad with Validation in Monadic Functions

functional programminghaskellmonadvalidation

I'm wondering what's better design wise for usability/maintainability, and what's better as far as fitting with the community.

Given the data model:

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)

I can implement monadic functions to transform the User for instance by adding items or stores etc, but I may end up with an invalid user so those monadic functions would need to validate the user they get and or create.

So, should I just:

  • wrap it in an error monad and make the monadic functions execute the validation
  • wrap it in an error monad and make the consumer bind a monadic validation function in the sequence that throws the appropriate error response (so they can choose not to validate and carry around an invalid user object)
  • actually build it into a bind instance on User effectively creating my own kind of error monad that executes validation with every bind automatically

I can see positives and negatives to each of the 3 approaches but want to know what is more commonly done for this scenario by the community.

So in code terms something like, option 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"]

option 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)

option 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"]

Best Answer

Fist I'd ask myself: Is having an invalid User a code bug or a situation that can normally occur (for example someone entering a wrong input to your application). If it's a bug, I'd try to ensure that it can never happen (like using smart constructors or creating more sophisticated types).

If it's a valid scenario then some error processing during runtime is appropriate. Then I'd ask: What does it really mean for me that a User is invalid?

  1. Does it mean that an invalid User can make some code fail? Do parts of your code rely on the fact that a User is always valid?
  2. Or does it just mean that its an inconsistency that needs to be fixed later, but doesn't break anything during the computation?

If it is 1., I'd definitely go for some kind of error monad (either standard or your own), otherwise you'll lose guarantees that your code is functioning properly.

Creating your own monad or using a stack of monad transformers is another issue, maybe this will be helpful: Has anyone ever encountered a Monad Transformer in the wild?.


Update: Looking at your expanded options:

  1. Looks as the best way to go. Perhaps, to be really safe, I'd rather hide the constructor of User and instead export only a few functions that don't allow to create an invalid instance. This way you'll be sure that any time it happens it gets handled properly. For example, a generic function for creating a User could be something like

    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.
    

    Many libraries take a similar appropach, for example Map, Set or Seq hide the underlying implementation so that it's not possible to create a structure that doesn't obey their invariants.

  2. If you postpone validation to the very end, and use Right ... everywhere, you don't need a monad any more. You can just do pure computations and resolve any possible errors at the end. IMHO this approach is very risky, as an invalid user value can lead to having invalid data elsewhere, because you didn't stop the computation soon enough. And, if it happens that some other method updates the user so that it's valid again, you'll end up with having invalid data somewhere and not even knowing about it.

  3. There are several problems here.

    • The most important is that a monad must accept any type parameter, not just User. So your validate would have to have type u -> ValidUser u without any restriction on u. So it's not possible to write such a monad that validates inputs of return, because return must be fully polymorhpic.
    • Next, what I don't understand is that you match on case return u of in the definition of >>=. The main point of ValidUser should be to distinguish valid and invalid values, and so the monad must ensure that this is always true. So it could be simply

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

    And this already looks very much like Either.

Generally, I'd use a custom monad only if

  • There are no existing monads that provide you with the functionality you need. Existing monads usually have many support functions, and more importantly, they have monad transformers so you can compose them into monad stacks.
  • Or if you need a monad that is too complex to describe as a monad stack.
Related Topic