Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 388
dmzkrsk Avatar asked Dec 20 '16 08:12

dmzkrsk


2 Answers

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

like image 132
Cirdec Avatar answered Oct 02 '22 16:10

Cirdec


You ask why use Monad if all you need is Applicative? I can ask -- why use Applicative if all you need is Monoid?

All you're doing is essentially trying to take advantage of monoidal behavior/a Monoid, but trying to do it through an Applicative interface. Kind of like working with Int's through their string representation (implementing + for the strings "1" and "12" and working with strings instead of just 1 and 12 and working with ints)

Note that you can get an Applicative instance from any Monoid instance, so finding a Monoid that can solve your problem is the same as finding an Applicative that can.

even :: Integer -> All
even x = All (rem x 2 == 0)

isSmall :: Integer -> All
isSmall x = All (x < 10)

greater :: (Integer, Integer) -> All
greater (a, b) = All (b > a)

To prove that they are the same, we can write conversion functions back and forth:

convertToMaybeFunc :: (a -> All) -> (a -> Maybe a)
convertToMaybeFunc f x = guard (getAll (f x)) $> x

-- assuming the resulting Just contains no new information
convertFromMaybeFunc :: (a -> Maybe b) -> (a -> All)
convertFromMaybeFunc f x = maybe (All False) (\_ -> All True) (f x)

You could directly write your validate:

validate :: Int -> All
validate a = isSmall a <> even a

But you could also write it in the lifted style you want:

validate :: Int -> All
validate = isSmall <> even

Want do notation?

validate :: Int -> All
validate = execWriter $ do
    tell isSmall
    tell even
    tell (other validator)

validate' :: (Int, Int) -> All
validate' = execWriter $ do
    tell (isSmall . fst)
    tell (isSmall . snd)
    tell greater

As you can see, every Monoid instance begets an Applicative/Monad instance (through Writer and tell), which makes this a bit convenient. You can think of Writer/tell here as "lifting" a Monoid instance to a free Applicative/Monad instance.

In the end, you're noticing a design pattern/abstraction that is useful, but it's really the monoid that you're noticing. You're fixated on working on that monoid through an Applicative interface, somehow...but it might be simpler to just work with the monoid directly.

Also,

validate :: Int -> All
validate = mconcat
  [ isSmall
  , even
  , other validator
  ]

is arguably comparable in clarity to the do notation version w/ Writer :)

like image 37
Justin L. Avatar answered Oct 02 '22 15:10

Justin L.