Boolean functors in lisp
Asked Answered
D

5

5

I find myself in a situation when I need to combine several predicate into one. Is there a standard way of doing this, something similar to compliment?

Suppose there are several simple predicates (e.g. is-fruit-p, is-red-p, grows-on-trees-p etc.) and a list of objects from which a subset must be filtered out using more than one predicate. What's the better way of achieving this than the following:

(remove-if #'is-fruit-p 
           (remove-if #'is-red-p 
                      (remove-if #'grows-on-trees-p list-of-objects)))
Dentelle answered 18/3, 2013 at 11:31 Comment(1)
(defun compliment (person &optional (stream t)) (format stream "Hello, ~a, you really look splendid today!" person))Demanding
A
3

I'm not sure if such function is available from the box. If you need to combine functions that you can determine in a compile time you can write a macro to do this. If you have to detect predicate functions dynamically you can write function to do this that will loop throw the list of functions and accumulate the results until false condition.

The macro can look like this:

(defmacro combine-predicates (combine-func &rest preds)
  (let ((x (gensym)))
    `(lambda (,x) (,combine-func ,@(loop for p in preds 
                      collecting `(funcall ,p ,x))))))

And you can use it like this

(remove-if (combine-predicates and 
                               #'is-fruit-p 
                               #'is-red-p 
                               #'grows-on-trees-p) obj-list)
Adriannaadrianne answered 18/3, 2013 at 12:6 Comment(3)
Thanks, what do you think of removing funcall and #'s?Dentelle
I'm not sure about how funcall affects performance so if you want, you can remove funcall. Actually I was worried that passing (lambda (x) (...)) won't work, but it did, at least in sbcl. But passing #'(lambda (x) (...)) didn't. If you don't use old lambda syntax than I think funcalls can be removed. Unless you want to use some compile time variable to setup predicate functions at compile time.Adriannaadrianne
Note that if you use Metatilities, this function already exists as conjoin.Humid
I
5

Are you sure that a special syntax would really help? Consider the following

(lambda (x)
  (and (is-fruit-p x)
       (or (grows-on-tree-p x)
           (is-red-p x))))

and now the slightly more general

(lambda (x)
  (and (is-fruit-p x)
       (or (grows-on-tree-p x)
           (eq (color x) 'red))))

or

(lambda (x)
  (and (is-fruit-p x)
       (or (grows-on-tree-p x)
           (eq (color x) desired-color)))) ; desired-color captured lexical

Even if you build up a special syntax for predicates do you think the added language complexity is worth the rigidity you will get? For example are you going to define a predicate #'weights-exactly-five-ounces-p? What about #'weights-up-to-and-including-six-and-half-ounces-p?

If you start needing a parametric predicate and use lambda forms for that then using a combiner you're going to write more code than not using it because the (lambda (x) ...) wrapper will be needed for each parametric term. More importantly that code will be also harder to read (in addition to having to learn a special new macro for predicate combination).

IMO it may make sense to write and/or combiners if you're passed in predicates and you need to pass predicates to someone else... but not for writing the code you used in the example; for that I'd write

(remove-if (lambda (x) (or (is-fruit-p x)
                           (is-red-p x)
                           (grows-on-trees-p x)))
           list-of-objects)

Less to write, less to read, nothing extra to learn, trivial to parametrize.

Suppose for example that you want a list of fruits with the same color as the one you have (in mine) and with same weight or possibly heavier...

(remove-if-not (lambda (x) (and (is-fruit-p x)
                                (eq (color x) (color mine))
                                (>= (weight x) (weight mine))))
               objects)
Imprisonment answered 18/3, 2013 at 13:40 Comment(2)
I doubt something like (remove-if (either is-red-p is-fruit-p) list) is difficult to read. Also I don't mind using code similar to one of your example, just that I have a biggish loop where I need to use remove-id with is-a-p, with is-b-p and with both is-a-p and is-b-p, and this gave me an idea that I could use some shortening.Dentelle
Could you please give elaborate a bit on how to parametrize your last example?Dentelle
U
4

Higher order functions like disjoin and conjoin are available in the quicklisp installable alexandria library.

CL-USER> (ql:quickload "alexandria")
...
CL-USER> (remove-if (alexandria:disjoin #'zerop #'oddp #'minusp)
                    '(0 -1 1 -2 2))
=> (2)
Underclothes answered 18/3, 2013 at 21:39 Comment(1)
Thanks, that's probably the best answer.Dentelle
A
3

I'm not sure if such function is available from the box. If you need to combine functions that you can determine in a compile time you can write a macro to do this. If you have to detect predicate functions dynamically you can write function to do this that will loop throw the list of functions and accumulate the results until false condition.

The macro can look like this:

(defmacro combine-predicates (combine-func &rest preds)
  (let ((x (gensym)))
    `(lambda (,x) (,combine-func ,@(loop for p in preds 
                      collecting `(funcall ,p ,x))))))

And you can use it like this

(remove-if (combine-predicates and 
                               #'is-fruit-p 
                               #'is-red-p 
                               #'grows-on-trees-p) obj-list)
Adriannaadrianne answered 18/3, 2013 at 12:6 Comment(3)
Thanks, what do you think of removing funcall and #'s?Dentelle
I'm not sure about how funcall affects performance so if you want, you can remove funcall. Actually I was worried that passing (lambda (x) (...)) won't work, but it did, at least in sbcl. But passing #'(lambda (x) (...)) didn't. If you don't use old lambda syntax than I think funcalls can be removed. Unless you want to use some compile time variable to setup predicate functions at compile time.Adriannaadrianne
Note that if you use Metatilities, this function already exists as conjoin.Humid
A
3

An approach using first class functions:

(defun complement (func)
  (lambda (x) (not (funcall func x))))

(defun conjoin (pred1 pred2)
  (lambda (x) (and (funcall pred1 x) (funcall pred2 x))))

(defun disjoin (pred1 pred2)
  (lambda (x) (or (funcall pred1 x) (funcall pred2 x))))

From which you can produce

(remove-if (conjoin #'is-fruit-p (conjoin #'is-red-p #'grows-on-trees-p)) list-of-objects)
Alanalana answered 18/3, 2013 at 14:13 Comment(0)
G
2
(let ((predicates '(zerop evenp)))
  (remove-if (lambda (item)
               (some (lambda (fn) (funcall fn item))
                     predicates))
             '(0 1 2 3 4 0 1 2 3 4)))
Glogau answered 18/3, 2013 at 13:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.