ruricolist / serapeum

Utilities beyond Alexandria
MIT License
428 stars 42 forks source link

condp (cond using predicate) ? #138

Open mmontone opened 2 years ago

mmontone commented 2 years ago

I recently had a look at Clojure's condp: https://clojuredocs.org/clojure.core/condp and I thought it could be handy for CL too, simplifies code.

Here is a slightly different implementation:

(defmacro condp (predicate &body clauses)
  "COND using PREDICATE."
  (let ((pred (gensym)))
    `(let ((,pred ,predicate))
       (cond
     ,@(loop for clause in clauses
         collect `((funcall ,pred ,(car clause))
               ,@(cdr clause)))))))

For example, this:

(defun object-kind (obj)
  (cond
    ((typep obj 'number) "number")
    ((typep obj 'string) "string")
    ((typep obj 'hashtable) "map")
    ((typep obj 'package) "module")
    ((typep obj 'class) "type")))

can be rewritten as:

(defun object-kind (obj)
  (condp (alexandria:curry #'typep obj)
    ('number "number")
    ('string "string")
    ('boolean "boolean")
    ('hash-table "map")
    ('package "module")
    ('class "type")))

Is there an idiom for this in CL or Serapeum already? Do you think this is worth including?

mmontone commented 1 year ago

What I like about this construct is that you can encode "custom pattern matchers" with it, and the resulting code is nice to read.

For example, a regex matcher:

(condp (alexandria:rcurry #'ppcre:scan "my-string")
   ("^foo" :foo)
   ("^bar" :bar)
   ("^my.*" :mine)
   ("^mi.*" :mio))

Or a string matcher:

(condp (alexandria:curry #'string= "some")
   ("foo" :foo)
   ("bar" :bar)
   ("some" :some))

(Now I'm thinking it could also be called match-with or similar).

What do you say @ruricolist ? Not interested?

ruricolist commented 1 year ago

I think this would be nice to have. I like condp better as a name; it arguably makes more sense in CL (with our -p predicates) than it does in Clojure.

ruricolist commented 1 year ago

Is there a reason not to have a separate expression argument, like Clojure does?

mmontone commented 1 year ago

Is there a reason not to have a separate expression argument, like Clojure does?

The reason is that I can control the order of arguments to the predicate using curry or rcurry as in the examples, and at which argument position the expression argument being tested is passed. I'm not sure how the Clojure version let's you control that. If you know, please let me know. I'll have a closer look.

ruricolist commented 1 year ago

I'm not sure if Clojure has it, but a combinator like flip could be used

On Mon, Jul 24, 2023, 7:56 PM Mariano Montone @.***> wrote:

Is there a reason not to have a separate expression argument, like Clojure does?

The reason is that I can control the order of arguments to the predicate using curry or rcurry as in the examples. I'm not sure how the Clojure version let's you control that. If you know, please let me know. I'll have a closer look.

— Reply to this email directly, view it on GitHub https://github.com/ruricolist/serapeum/issues/138#issuecomment-1648812705, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAC7AKCTXDQN7W3NF3WEN2TXR4KUNANCNFSM57HVK72A . You are receiving this because you were mentioned.Message ID: @.***>

mmontone commented 1 year ago

I thought about that, but by composing a predicate without a separate expression, I have complete control over how I can compose the predicate, and which arguments doesn't matter how many and where to pass them. And so I didn't see the point on separating predicate and expression.

mmontone commented 1 year ago

With the most extreme case of using a lambda:

(condp (lambda (option) (string= option "my expression"))
     ("lala" :no)
     ("my expression" :yes))

I suppose it is a question of readability, as the predicate-only option is very flexible.