An Applicative for Transactional Validation

By Phil Freeman · June 20, 2019

Suppose you would like to perform multiple validations on your data before you write it to the database or some other store. Some likely constraints are as follows:

  • You would like all of the validations to pass before any data is written
  • If there are multiple validation errors, you would like to receive all of them
  • It should be possible to easily compose such actions out of smaller parts

In a relational database, there is a simple solution which satisfies the constraints: open a transaction, perform all validations and writes in the transaction, and commit. We can interleave writes with validations, because any failure to validate will roll back all writes.

While the transactional approach is probably the right way in most cases, there can be cases where we might be happier with a less robust design, and willing to trade off some correctness for flexibility. For example, we might not even have access to transactions (perhaps we are making API calls, instead of writing directly to the database) or we might want to perform validations in the application code instead of in the database.

I’m going to explain a simple technique we’ve been using at Lumi in these cases.

An initial API

A computation which does some validation and then writes to the database (or performs some other IO) could be described by the following types:

prepare :: Validation err prepared
commit :: prepared -> IO result

Here I’m using the Validation type from the validation package, so that we can capture all validation errors at once.

We can package this data up as a data type:

data Action err prepared result = Action
 { prepare :: Validation err prepared
 , commit :: prepared -> IO result
 }

Reducing the number of type variables

The type variable prepared is actually unimportant - any function that interprets an Action could reasonably be expected to ignore the type of the intermediate prepared result - so let’s existentially quantify over it:

{-# LANGUAGE GADTs #-}

data Action err result where
  Action :: Validation err prepared
         -> (prepared -> IO result)
         -> Action err result

This type is isomorphic to the following type, where we have removed prepared entirely, by fmapping the second function over the result of the first:

data Action err result = Action
  { runAction :: Validation err (IO result) 
  }

At this point, we don’t need to define our own type at all, since we can reuse the Compose type from base:

type Action err = Compose (Validation err) IO

This presentation is convenient, because we can use the Hackage documentation to discover all sorts of helpful type class instances. In particular, the composition of two Applicative functors is also Applicative:

(Applicative f, Applicative g) => Applicative (Compose f g)

If the outermost constructor is Alternative, then so is the composition:

(Alternative f, Applicative g) => Alternative (Compose f g)

Since Validation err and IO are both Applicative, we get an instance:

Semigroup err => Applicative (Action err)

We don’t quite get Alternative, because Validation err is not an Alternative, but we do get Alt from semigroupoids:

Alt (Action err)

These two instances give us two simple and convenient ways to compose actions together. We can combine two actions using <*> (or even applicative do notation) and require both validations to succeed before either IO action is initiated. Alternatively (pun intended), we can have one action fall back to a second action in the case of a validation failure, by using <!>.

Example

Suppose we have a simple Person data type:

data Person = Person
  { firstName :: Text
  , lastName :: Text
  }

and a way to write a Person to our data store:

writePerson :: Person -> IO Person

We can build an Action which performs a validated write:

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE RecordWildcards #-}

writePersonAction :: Person -> Action Errors Person
writePersonAction p = Compose $ do
  firstName <- validateFirstName (firstName p)
  lastName <- validateLastName (lastName p)
  pure (writePerson Person{..})

Here, we’re using the Applicative instance for Validation, but we can also use Applicative to combine two Actions:

multipleWrites :: Person -> Person -> Action Errors (Person, Person)
multipleWrites p1 p2 = 
  (,) <$> writePersonAction p1
      <*> writePersonAction p2

We can even use Traversable to perform bulk database updates:

bulkWrite :: [Person] -> Action Errors [Person]
bulkWrite = traverse multipleWrites

In this final example, no writes will be performed if any Person structure has a validation error!

Lumi is accelerating the world's transition to a circular economy. Want to work with us? We're hiring.

RSS · Github