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?User
can make some code fail? Do parts of your code rely on the fact that aUser
is always valid?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:
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 aUser
could be something likeMany libraries take a similar appropach, for example
Map
,Set
orSeq
hide the underlying implementation so that it's not possible to create a structure that doesn't obey their invariants.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.There are several problems here.
User
. So yourvalidate
would have to have typeu -> ValidUser u
without any restriction onu
. So it's not possible to write such a monad that validates inputs ofreturn
, becausereturn
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 ofValidUser
should be to distinguish valid and invalid values, and so the monad must ensure that this is always true. So it could be simplyAnd this already looks very much like
Either
.Generally, I'd use a custom monad only if