Validating Form Data via Applicative Functors

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.

Initial goal

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

The problem here is that ExceptT exits quickly, as soon as it encounters the first error:

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.

The search

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:

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!

The problem

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 using Compose, there is no need to define both a regular and a transformer version of Validation.

Ok, there are few things to unpack here!

No Monad instance

I 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 type err or a, similar to Either. However, the Applicative instance for Validation accumulates errors using a Semigroup on err. In contrast, the Applicative for Either returns only the first error.

A consequence of this is that Validation has no Bind or Monad instance. This is because such an instance would violate the law that a Monad's ap must equal the Applicative'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:


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:

Compare it with ExceptT:

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:

It outputs this:

All right, I've learned few new things, I hope you have as well, this was fun!

Addition 1: newtype vs Compose

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

You can now just replace your Compose m (Validation err) a with ValidateT err m a everywhere.

Addition 2: ApplicativeDo and thoughts on using a Monad

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.