0

I have two predicates

interface Foo {}
interface Bar {}
declare const isFoo: (a:unknown):a is Foo
declare const isBar: (a:unknown):a is Bar

What is the functional way to combine two predicates to create a new predicate (for simplicity, let's assume it's a => isFoo(a) && isBar(a)?

With fp-ts, I initially thought I could fold(monoidAll)([isFoo, isBar]), but fold expects the array to be of booleans, not of functions that evaluate to boolean.

This works

import { monoid as M, function as F, apply as A, identity as I, reader as R } from 'fp-ts'

interface Foo{}
interface Bar{}

declare const isFoo:(a:unknown) => a is Foo
declare const isBar:(a:unknown) => a is Bar

const isFooAndBar = F.pipe(A.sequenceT(R.reader)(isFoo, isBar), R.map(M.fold(M.monoidAll)))

But boy howdy is that convoluted. I thought there could be another way. I ended up writing my own monoid that takes two predicates and combines them, calling it monoidPredicateAll:

const monoidPredicateAll:M.Monoid<Predicate<unknown>> = {
  empty: ()=>true,
  concat: (x,y) => _ => x(_) && y(_)
}

Is there a canonical FP way of combining two predicates? I know I could do something like

xs.filter(x => isFoo(x) && isBar(x))

But it can get complicated with more predicates, and re-using a monoid makes it less likely I'll do a typo like isFoo(x) || isBar(x) && isBaz(x) when I meant all && (and that's where a xs.filter(fold(monoidPredicateAll)(isFoo,isBar,isBaz)) would help out.

I found a discussion about this on SO, but it was about Java and a built-in Predicate type, so didn't directly address my question.

Yes, I'm overthinking this :)

user1713450
  • 1,307
  • 7
  • 18
  • 1
    Yes, using a monoid instance is the best solution to your problem. And yes, your monoid instance for predicates is correct. https://hackage.haskell.org/package/base-4.14.0.0/docs/src/Data.Functor.Contravariant.html#line-180 – Aadit M Shah Dec 09 '20 at 03:10
  • 1
    Just in case you don't know it here is an [article](https://medium.com/@drboolean/monoidal-contravariant-functors-are-actually-useful-1032211045c4) about contravariant functors which includes a section about a `Predicate` type where the author composes complex predicates out of simple ones. It involves monoids and contravariant functors, both of which are very useful for such a type. –  Dec 09 '20 at 08:42

2 Answers2

1

I ended up doing this:

export const monoidPredicateAll:Monoid<Predicate<unknown>> = {
    empty: ()=>true,
    concat: (x,y) => _ => x(_) && y(_)
}

Then I could do

import {monoid as M} from 'fp-ts'
declare const isFoo: Predicate<number>
declare const isBar: Predicate<number>

const isFooAndBar = M.fold(monoidPredicateAll)([isFoo,isBar])
user1713450
  • 1,307
  • 7
  • 18
  • Your example give a compile error `Type 'Predicate' is not assignable to type 'Predicate'.` Could you give an example / update your answer to make the monoid generic? – florian norbert bepunkt Sep 21 '21 at 17:26
  • It does not give a compile error. I just pasted all this code into my IDE and it has no errors. I suspect you are not using it right. – user1713450 Oct 04 '21 at 18:43
0

For others looking for a working solution, based on @user1713450's answer

import * as P from 'fp-ts/lib/Predicate';
import * as M from 'fp-ts/Monoid';

const createMonoidPredicateAll = <T>(): M.Monoid<P.Predicate<T>> => ({
  empty: () => true,
  concat: (x, y) => (_) => x(_) && y(_),
});

export const combine = <T>(predicates: P.Predicate<T>[]) =>
  M.concatAll(createMonoidPredicateAll<T>())(predicates);



florian norbert bepunkt
  • 2,099
  • 1
  • 21
  • 32
  • Because you only use T in the return type, it's unnecessary. `unknown` works, and you've got something configured wrong on your end if it doesn't work for you. Generics are only useful if you're using them in multiple places to enforce them being of the same type. If you're only using a generic in a single place, you can replace it with `unknown`. – user1713450 Oct 06 '21 at 03:23