Search code examples
haskellcompositionapplicative

Combining validators in applicative style in Haskell


I have a good grasp on imperative programming, but now I learn myself a Haskell for great good.

I think, I have a good theoretical understanding of Monads, Functors and Applicatives, but I need some practice. And for practice I sometimes bring some bits from my current work tasks.

And I'm stuck a bit with combining stuff in applicative way

First question

I have two functions for validation:

import Prelude hiding (even)

even :: Integer -> Maybe Integer
even x = if rem x 2 == 0 then Just x else Nothing

isSmall :: Integer -> Maybe Integer
isSmall x = if x < 10 then Just x else Nothing

Now I want validate :: Integer -> Maybe Integer built from even and isSmall

My best solution is

validate a = isSmall a *> even a *> Just a

And it's not point free

I can use a monad

validate x = do
  even x
  isSmall x
  return x

But why use Monad, if (I suppose) all I need is an Applicative? (And it still not point free)

Is it a better (and more buitiful way) to do that?

Second question

Now I have two validators with different signatures:

even = ...

greater :: (Integer, Integer) -> Maybe (Integer, Integer)
-- tuple's second element should be greater than the first
greater (a, b) = if a >= b then Nothing else Just (a, b)

I need validate :: (Integer, Integer) -> Maybe (Integer, Integer), which tries greater on the input tuple and then even on the tuple's second element.

And validate' :: (Integer, Integer) -> Maybe Integer with same logic, but returning tuple's second element.

validate  (a, b) = greater (a, b) *> even b *> Just (a, b)
validate' (a, b) = greater (a, b) *> even b *> Just  b

But I imagine that the input tuple "flows" into greater, then "flows" into some kind of composition of snd and even and then only single element ends up in the final Just.

What would a haskeller do?


Solution

  • When you are writing validators of the form a -> Maybe b you are more interested in that whole type than in the Maybe applicative. The type a -> Maybe b are the Kleisli arrows of the Maybe monad. You can make some tools to help work with this type.

    For the first question you can define

    (>*>) :: Applicative f => (t -> f a) -> (t -> f b) -> t -> f b
    (f >*> g) x = f x *> g x
    
    infixr 3 >*>
    

    and write

    validate = isSmall >*> even
    

    Your second examples are

    validate = even . snd >*> greater
    validate' = even . snd >*> fmap snd . greater
    

    These check the conditions in a different order. If you care about evaluation order you can define another function <*<.

    ReaderT

    If you use the type a -> Maybe b a lot it might be worth creating a newtype for it so that you can add your own instances for what you want it to do. The newtype already exists; it's ReaderT, and its instances already do what you want to do.

    newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }
    

    When you use the type r -> Maybe a as a validator to validate and transform a single input r it's the same as ReaderT r Maybe. The Applicative instance for ReaderT combines two of them together by applying both of their functions to the same input and then combining them together with <*>:

    instance (Applicative m) => Applicative (ReaderT r m) where
        f <*> v = ReaderT $ \ r -> runReaderT f r <*> runReaderT v r
        ...
    

    ReaderT's <*> is almost exactly the same as >*> from the first section, but it doesn't discard the first result. ReaderT's *> is exactly the same as >*> from the first section.

    In terms of ReaderT your examples become

    import Control.Monad.Trans.ReaderT
    
    checkEven :: ReaderT Integer Maybe Integer
    checkEven = ReaderT $ \x -> if rem x 2 == 0 then Just x else Nothing
    
    checkSmall = ReaderT Integer Maybe Integer
    checkSmall = ReaderT $ \x -> if x < 10 then Just x else Nothing
    
    validate = checkSmall *> checkEven
    

    and

    checkGreater = ReaderT (Integer, Integer) Maybe (Integer, Integer)
    checkGreater = ReaderT $ \(a, b) = if a >= b then Nothing else Just (a, b)
    
    validate = checkGreater <* withReaderT snd checkEven
    validate' = snd <$> validate
    

    You use one of these ReaderT validators on a value x by runReaderT validate x