I've had an interesting mini-journey while I was in search of a way to validate input data in Haskell recently, and ended up implementing one myself, and then got an interesting comment on reddit, which I will unwrap in this post.
At first, all I wanted to get was a way to validate my input data structure into another, resulting one. Something which can be done via ExceptT like this (exceptt.hs):
x#!/usr/bin/env stack-- stack --resolver=lts-13.1 script --package text,transformersimport Control.Monad.Trans.Exceptimport qualified Data.Text as Timport Data.Text (Text)newtype ValidUrl = ValidUrl Text deriving (Show, Eq)data InputForm = InputForm { inpUsername :: Text , inpHomepage :: Text } deriving (Show, Eq)data OutputForm = OutputForm { outUsername :: Text , outPassword :: ValidUrl } deriving (Show, Eq)lengthBetween :: Monad m => Int -> Int -> Text -> ExceptT Text m TextlengthBetween n m txt = if T.length txt < n || T.length txt > m then throwE ("Length must be between " <> T.pack (show n) <> " and " <> T.pack (show m)) else return txtvalidateUrl :: Monad m => Text -> ExceptT Text m ValidUrlvalidateUrl txt -- exercise to a reader = throwE "Invalid URL"main :: IO ()main = do let inpForm = InputForm "usr" "httpbadurl" outForm <- runExceptT $ OutputForm <$> lengthBetween 4 20 (inpUsername inpForm) <*> validateUrl (inpHomepage inpForm) print outFormThe problem here is that ExceptT exits quickly, as soon as it encounters the first error:
xxxxxxxxxx$ ./exceptt.hsLeft "Length must be between 4 and 20"
What I want instead is to gather all the error messages with their field names, so that my front-end could show them all nicely.
I was looking for a type that is similar to ExceptT, I would assume the name to be ValidateT, so this was what I hoogled for. Surprisingly, all I could find was the Validation type, but not its transformer version.
So, at first I've implemented my own ValidationT and made a PR which I encourage you to read. I won't quote the implementation, only the test case scenario:
xxxxxxxxxx... , testCase "combine two ValidateT" $ do v1 <- runValidationT $ ((,) <$> ValidationT (pure (Failure ["first"])) <*> ValidationT (pure (Failure ["second"]))) :: IO (Validation [String] ((), ())) assertEqual "errors get accumulated" v1 (Failure ["first", "second"])...As you can see, it does what I want. So, what could be the problem? I had a gut feeling that there must be one, so I've asked reddit about it!
That's where the wonderful comment by Samuel Gélineau came in:
Notice that
Validationintentionally doesn't have a Monad instance! For this reason, it doesn't make much sense to make it into a Monad transformer. An Applicative transformer would make more sense, but since Applicatives compose usingCompose, there is no need to define both a regular and a transformer version ofValidation.
Ok, there are few things to unpack here!
Validation intentionally has no Monad instanceI missed that, because I used ExceptT as the base for my implementation, which indeed has one. So, why exactly does it not have one? The docs mention it, actually:
An
Validationis either a value of the typeerrora, similar toEither. However, theApplicativeinstance forValidationaccumulates errors using aSemigrouponerr. In contrast, theApplicativeforEitherreturns only the first error.A consequence of this is that
Validationhas noBindorMonadinstance. This is because such an instance would violate the law that a Monad'sapmust equal theApplicative's<*>An example of typical usage can be found here.
Interesting. I've never seen the Bind class before. Data.Functor.Bind has not only this Bind, but also Apply (which is similar to Bind but for Applicative's <*>). Good to know :)
So, the ValidationT type I've implemented in my PR violates the law that its <*> must be the same as the ap from Monad. But what if I were to only implement an Applicative instance? Turns out there is no need to do so! That's what the second part of the comment is telling:
Compose, there is no need to define both a regular and a transformer version of Validation.Right, so let's try it out and see if we can just use an existing thing called Compose to build our ValidationT-like functionality. Compose looks like this:
xxxxxxxxxxnewtype Compose f g a = Compose { getCompose :: f (g a) }Compare it with ExceptT:
xxxxxxxxxxnewtype ExceptT e m a = ExceptT (m (Either e a))In our usage, we will put IO under f, and Validation under g. Here are the new relevant parts of our code. I've kept throwE reimplemented under the same name outside for clarity. Full code is at compose.hs:
xxxxxxxxxxthrowE :: Applicative m => err -> Compose m (Validation err) athrowE err = Compose (pure (Failure err))lengthBetween :: Applicative m => Int -> Int -> Text -> Compose m (Validation [Text]) TextlengthBetween n m txt = if T.length txt < n || T.length txt > m then throwE [ "Length must be between " <> T.pack (show n) <> " and " <> T.pack (show m) ] else pure txtvalidateUrl :: Applicative m => Text -> Compose m (Validation [Text]) ValidUrlvalidateUrl txt -- exercise to a reader = throwE ["Invalid URL"]main :: IO ()main = do let inpForm = InputForm "usr" "httpbadurl" outForm <- getCompose $ OutputForm <$> lengthBetween 4 20 (inpUsername inpForm) <*> validateUrl (inpHomepage inpForm) print outFormIt outputs this:
xxxxxxxxxx./compose.hsFailure ["Length must be between 4 and 20","Invalid URL"]
All right, I've learned few new things, I hope you have as well, this was fun!
Now, after having some usage in real world, I wanted to add few more things. First, turns out that using Compose without a newtype might not be the best thing to do. Type errors become hard to interpret, because you use type alias in your annotations, but you get unwrapped types in your errors. Here's how to do the newtype (ful code newtype.hs):
xxxxxxxxxxnewtype ValidateT err m a = ValidateT (Compose m (Validation err) a) deriving (Functor, Applicative)runValidateT :: Applicative m => ValidateT err m a -> m (Validation err a)runValidateT (ValidateT c) = getCompose cYou can now just replace your Compose m (Validation err) a with ValidateT err m a everywhere.
I have.some very big forms to validate, and using Applicative syntax isn't a very nice looking thing, so I've decided to try the ApplicativeDo extension. Here's how it looks (applicativedo.hs):
runValidateT $ do
l <- lengthBetween 4 20 (inpUsername inpForm)
url <- validateUrl (inpHomepage inpForm)
pure (OutputForm l url)
Looks fantastic, doesn't it? Well, in real life, turns out there are few limitations. First, this code won't work:
runValidateT $ do
l <- lengthBetween 4 20 (inpUsername inpForm)
url <- validateUrl (inpHomepage inpForm)
-- this breaks:
let res = OutputForm l url
pure res
You can't use let-expressions in ApplicativeDo blocks. It's hard to see why this is slightly annoying, but when your form is really big, it makes a difference.
Another thing is, as it turns out, there are more dependencies that I have in my form logic than I expected. So, maybe I do want it to just be a Monad which lets you do Writer-like accumulation of form errors. I think I might switch to one. Again, the reddit topic mentions few interesting ones.
Please send your feedback in Issues or PRs in this blog's repo.