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,transformers
import Control.Monad.Trans.Except
import qualified Data.Text as T
import 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 Text
lengthBetween 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 txt
validateUrl :: Monad m => Text -> ExceptT Text m ValidUrl
validateUrl 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 outForm
The problem here is that ExceptT
exits quickly, as soon as it encounters the first error:
xxxxxxxxxx
$ ./exceptt.hs
Left "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
Validation
intentionally 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
Validation
is either a value of the typeerr
ora
, similar toEither
. However, theApplicative
instance forValidation
accumulates errors using aSemigroup
onerr
. In contrast, theApplicative
forEither
returns only the first error.A consequence of this is that
Validation
has noBind
orMonad
instance. This is because such an instance would violate the law that a Monad'sap
must 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:
xxxxxxxxxx
newtype Compose f g a =
Compose {
getCompose :: f (g a)
}
Compare it with ExceptT
:
xxxxxxxxxx
newtype 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:
xxxxxxxxxx
throwE :: Applicative m => err -> Compose m (Validation err) a
throwE err = Compose (pure (Failure err))
lengthBetween ::
Applicative m => Int -> Int -> Text -> Compose m (Validation [Text]) Text
lengthBetween 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 txt
validateUrl :: Applicative m => Text -> Compose m (Validation [Text]) ValidUrl
validateUrl 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 outForm
It outputs this:
xxxxxxxxxx
./compose.hs
Failure ["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):
xxxxxxxxxx
newtype 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 c
You 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.