4

As a follow up to my previous question, I am trying to implement a simple pattern matching in Clojure.

I would like something like the following:

(match target
  [ub]    expr1    ; ub should be bound to actual value in expr1
  ['< ub] expr2    ; match the literal less-than symbol
                   ; and ub should be bound to actual value in expr2
  [lb ub] expr3    ; lb and ub should be bound to actual values in expr3
  :else   expr4    ; default case if none match
)

Usage:

(match [< 5.0] ...)

should arrange to execute expr2 at runtime.

I would like to write a macro, but I am not sure of the expansion.

I am considering having each case-and-clause expand to a let with bindings to internal variables and checks that the literal symbols ('<) actually matches the pattern. Maybe for the second pattern (['< ub]):

(let [[sym1 ub] pattern]
  (if (= '< sym1)
    expr1)

Do I need to use (gensym) for the bindings? How?

Bigger picture:

(range-case target
            [0.0 < 1.0] :greatly-disagree
            [< 2.0]     :disagree
            [< 3.0]     :neutral
            [< 4.0]     :agree
            [5.0]       :strongly-agree
            42          :the-answer
            :else       :do-not-care)

I am trying to match the [...] patterns and convert them to the following:

[ub]          (if previous-ub `(and (<= ~previous-ub ~target) (<= ~target ~ub))
                              `(< ~target ~ub))
['< ub]       (if previous-ub `(and (<= ~previous-ub ~target) (< ~target ~ub))
                              `(< ~target ~ub))
[lb ub]       `(and (<= ~lb ~target) (<= ~target ~ub))
['< lb ub]    `(and (< ~lb ~target) (<= ~target ~ub))
[lb '< ub]    `(and (<= ~lb ~target) (< ~target ~ub))
['< lb '< ub] `(and (< ~lb ~target) (< ~target ~ub))

I have a cond that checks that the case part is a vector. This pattern match should occur inside that case.

3
  • have you looked around at the existing pattern matching tools for Clojure? matchure for example looks pretty interesting: spin.atomicobject.com/2010/04/25/… Commented Jun 14, 2011 at 19:40
  • @mikera: Yeah. I'm just trying to figure macros out and hope that writing them will shed some light. Commented Jun 14, 2011 at 19:53
  • ah that's cool then. reinventing wheels is perfectly fine as long as you are trying to learn the art of wheelmaking! Commented Jun 14, 2011 at 19:56

1 Answer 1

3

My first idea was basically the same: Bind stuff to internal locals and test on their contents in a big and. For literals the value is bound to a generated local; symbols are used directly in the binding.

I also added a check that the spec vector matches the length of the target vector. Otherwise you can't have [ub] as well as [lb ub] since neither contains a check which could fail. So always the first would be selected.

Here is the code:

(defn make-clause
  [expr-g [spec expr & more :as clause]]
  (when (seq clause)
    (let [tests-and-bindings (map (fn [x]
                                    (if-not (symbol? x)
                                      (let [x-g (gensym "x")]
                                        [`(= ~x ~x-g) x-g])
                                      [nil x]))
                                  spec)
          tests    (keep first tests-and-bindings)
          bindings (map second tests-and-bindings)]
      `(let [[~@bindings] ~expr-g]
         (if (and (= (count ~expr-g) ~(count spec)) ~@tests)
           ~expr
           ~(make-clause expr-g more))))))

(defmacro match
  [expr & clauses]
  (let [expr-g  (gensym "expr")]
    `(let ~[expr-g expr]
       ~(make-clause expr-g clauses))))

And an example expansion. I didn't use syntax-quote in the example to reduce the noise in the expansion, but you should get the idea.

(let [expr98 [(quote <) 3.0]]
  (let [[ub] expr98]
    (if (and (= (count expr98) 1))
      (if previous-ub
        (and (<= previous-ub target) (<= target ub))
        (< target ub))
      (let [[x99 ub] expr98]
        (if (and (= (count expr98) 2) (= (quote <) x99))
          (if previous-ub
            (and (<= previous-ub target) (< target ub))
            (< target ub))
          (let [[lb ub] expr98]
            (if (and (= (count expr98) 2))
              (and (<= lb target) (<= target ub))
              nil)))))))

The invokation was:

(match ['< 3.0]
  [ub]    (if previous-ub
            (and (<= previous-ub target) (<= target ub))
            (< target ub))
  ['< ub] (if previous-ub
            (and (<= previous-ub target) (< target ub))
            (< target ub))
  [lb ub] (and (<= lb target) (<= target ub))))

Hope that helps you get started.

Sign up to request clarification or add additional context in comments.

8 Comments

@kotarak: I thought about the problem at 1AM while staring at the ceiling :-). I'm going to try out my ideas before exploring yours to see if I can get it to work. After all, this whole thing is an exercise in learning macros (mostly). My idea is to traverse the list of patterns/expressions and form cond clauses with a test for the literals and a let block for the bindings. BTW, "Let Over Lambda", by Doug Hoyte, while written for Common Lisp, is giving me some great ideas for macros in Clojure.
@ralph you should definitively explore your ideas! I decided against a cond, BTW. Maybe you have similar considerations but decide for it. Let's see. All-in-all I'm pretty happy that there are macros available, but found that I don't use them thaaat often in Clojure.
@ralph and in the end have a look at matchjure to see how the experts do it. ;)
@kotarak: Looking at your expansion, I see that each new pattern is nested inside the previous one. That means that each pattern can "see" the bindings from the previous one. Shadowing will hide the overridden symbols, but the other bindings are still visible. That may or may not be a problem, depending on how you want it to behave. I was considering ways of nesting the let blocks so that each pattern hides its bindings from subsequent ones. Maybe that is the reason your chose if statements instead of cond.
@kotarak: "I also added a check that the spec vector matches the length of the target vector." I was also thinking about this last night. Another extension might be to support [item item ... :when condition] and allow patterns similar to those in Scala. The item count would have to match the target count. You could also allow :val symbol that gets replaced with the symbol's current binding. My idea is growing into a full implementation of matchure :-).
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.