DEV Community

loading...
Cover image for Composing predicates

Composing predicates

gillchristian profile image Christian Gill Originally published at collectednotes.com Updated on ・2 min read

A few days ago I was writing some validation functions that were basically a composition of several other predicates.

Which would look something like this:

predA :: a -> Bool

predB :: a -> Bool

predC :: a -> Bool

predABC :: a -> Bool
predABC a = predA a && predB a && predC a

A Haskeller would be bothered by having to manually pass a to all the predicates. A true Haskeller would not, probably. But I'm far from that level of enlightenment. So I decided to find a way to compose predicates.

Note that by compose I mean not function composition but to combine two predicates together to get a new one. Either by conjunction (&&) or disjunction (||).

Monoids

Since Monoids let us combine things, they were bound to show up in my search.

As we said, booleans (and predicates) can be combined in more than one way, so there isn't a default Monoid instance for Bool.

Data.Monoid defines:

  • All for conjunction.
  • Any for disjunction.
λ> getAll (All True <> All False)
False
λ> getAll (All True <> All True)
True
λ> getAny (Any True <> Any False)
True
λ> getAny (Any False <> Any False)
False

Since the goal is to compose not booleans but predicates, we need to some mapping and folding to achieve that.

predABC :: a -> Bool
predABC = getAll . foldMap (All .) [predA, predB, predC]
λ> predABC a
True
λ> predABC b
True
λ> predABC z
False

Success? Well, it works and we can add as many predicates as we like with very low effort. But to compose just three predicates I'm not sure if it's worth.

Applicative

If there are two ways to do this, there has to be a third.

From #100DaysOfFP Day 33

Applicative instance of (->) functions allows to apply an argument (x) to two unary functions (f & g) and then apply the result of each to a third function (h) to produce the result (y).

That's exactly what I want!

(&&&) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
f &&& g = (&&) <$> f <*> g

(|||) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
f ||| g = (||) <$> f <*> g

Composing the predicates is way simpler now

predABC :: a -> Bool
predABC = predA &&& predB &&& predC

And is possible to use both combinators. Which was not possible with Monoids (most probably is possible but not as straightforward).

predAandBorC :: a -> Bool
predAandBorC = predA &&& predB ||| predC

Yup, I'm going to stick with those for combining my predicates 😏

One more thing, I declared the types of (&&&) and (|||) specified for the function instance of applicative. But they could work with any applicative.

(&&&) :: Applicative f => f Bool -> f Bool -> f Bool
f &&& g = (&&) <$> f <*> g

(|||) :: Applicative f => f Bool -> f Bool -> f Bool
f ||| g = (||) <$> f <*> g
λ> (Just True) &&& (Just False)
Just False
λ> (Just True) &&& (Just True)
Just True
λ> (Just False) &&& (Just False)
Just False

Yet another thing. liftA2 could make the implementations even shorter.

(&&&) :: Applicative f => f Bool -> f Bool -> f Bool
(&&&) = liftA2 (&&)

(|||) :: Applicative f => f Bool -> f Bool -> f Bool
(|||) = liftA2 (||)

Happy and safe coding 🦄

Discussion (6)

pic
Editor guide
Collapse
sshine profile image
Simon Shine

I’ve previously called them <&&> and <||> since they’re based on the Applicative instance of (->) e and other Applicative operators like <$> and <*> look like this. Also, &&& is used in Control.Arrow, so this avoids overlap.

Collapse
gillchristian profile image
Christian Gill Author

Good point.

I should've done a Hoogle search. I see now that several packages define <&&> & <||>.

Thanks for pointing those out.

Collapse
macsikora profile image
Maciej Sikora

Really nice showing of the good way of operator overloading. Good job!

Collapse
gillchristian profile image
Christian Gill Author

Operators ares just functions, right? 😅

Collapse
macsikora profile image
Maciej Sikora

Yep but your example shows how we can make monoids from anything we want really and also give nice syntax to it.

Collapse
riccardoodone profile image
Riccardo Odone

Wow, this is a bomb! Loved it!