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 :-).
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.
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
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