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
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?
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?
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 <*<
.
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
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 :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With