Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Turn monadic validation into applicative?

I need to validate that numeric input is in a certain range. To this end, I'm using

ensure :: (a -> Bool) -> a -> Maybe a
ensure p v | p v       = Just v
           | otherwise = Nothing

One way to check the upper and lower bound of some value x :: Int is via monadic chaining:

let validated = pure x >>= ensure (>0) >>= ensure (<100)

To my understanding, the order of the two validations does not matter; hence, it should be possible to rewrite the above expression in applicative form. How? I did not manage to do it, but I'm hoping to gain a deeper understanding of applicatives once I do :-).

like image 686
Ulrich Schuster Avatar asked Aug 15 '26 21:08

Ulrich Schuster


2 Answers

The very subtle thing to note here is that "intuitively" the return value of ensure doesn't matter. It only matters whether it's Just or Nothing. To express that, you might give it a more honest type signature:

ensure :: (a -> Bool) -> a -> Maybe ()
ensure p v | p v       = Just ()
           | otherwise = Nothing

Now, since the return value doesn't matter, you should be safe to ignore it. And so the logic becomes this: call ensure (>0) and ensure (<100), then combine them together, ignoring their return values, but preserving their Maybe shapes.

To do that, you can indeed use applicative. And the function that you're going to apply will do just what I said above: ignore both return values. The "preserve Maybe shapes" part will be handled by the Applicative instance transparently. So:

let validated = (\_ _ -> x) <$> ensure (>0) x <*> ensure (<100) x

See how my applied function ignores both parameters and returns x itself?


But of course, the type signature of ensure, as written in your question, suggests that it may be doing something more than just validation. Of course it really can't when it's completely generic, but it could if it was a bit more concrete:

ensure :: (Int -> Bool) -> Int -> Maybe Int
ensure p v | p v       = Just (v + 100)
           | otherwise = Nothing

And now the order suddenly matters:

pure (-5) >>= ensure (>0) >>= ensure (<100) == Nothing
pure (-5) >>= ensure (<100) >>= ensure (>0) == Just 95

So the bottom line here is: the purpose of ensure is a bit ambiguous. Does it really return Maybe a? Then the order might matter. Is the true return type Maybe ()? Then you can use Applicative and ignore those units.

like image 136
Fyodor Soikin Avatar answered Aug 18 '26 20:08

Fyodor Soikin


You can use liftA2 to combine two predicates:

> import Control.Applicative
> :t liftA2 (&&) (> 0) (< 100)
liftA2 (&&) (> 0) (< 100) :: (Ord a, Num a) => a -> Bool

This function has the right type to be used with ensure:

validate :: Num a => a -> Bool
validate f g = ensure (liftA2 (&&) f g)

Then

> validate (> 0) (< 100) 50
Just 50
> validate (> 0) (< 100) 1000
Nothing
like image 32
chepner Avatar answered Aug 18 '26 18:08

chepner



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!