Clojure 范围大小写宏
在《R. Kent Dybvig 所著的 “Scheme 编程语言,第 4 版” 一书中,第86 中,作者为接受其条件范围的 case
语句编写了一个 define-syntax
(Scheme 宏)。我想我会在 Clojure 中尝试这个。
这是结果。
我该如何改进这个?我使用 :ii
、:ie
、:ei
和 :ee
作为范围运算符,表示包含 -包容性、包容性、排他性、排他性、 和独家独家。还有更好的选择吗?
我选择扩展为 cond
而不是离散的 if
语句,因为我觉得我将从未来对 cond
宏的改进中获益。
(defmacro range-case [target & cases]
"Compare the target against a set of ranges or constant values and return
the first one that matches. If none match, and there exists a case with the
value :else, return that target. Each range consists of a vector containing
3 terms: a lower bound, an operator, and an upper bound. The operator must
be one of :ii, :ie, :ei, or :ee, which indicate that the range comparison
should be inclusive-inclusive, inclusive-exclusive, exclusive-inclusive,
or exclusive-exclusive, respectively.
Example:
(range-case target
[0.0 :ie 1.0] :greatly-disagree
[1.0 :ie 2.0] :disagree
[2.0 :ie 3.0] :neutral
[3.0 :ie 4.0] :agree
[4.0 :ii 5.0] :strongly-agree
42 :the-answer
:else :do-not-care)
expands to
(cond
(and (<= 0.0 target) (< target 1.0)) :greatly-disagree
(and (<= 1.0 target) (< target 2.0)) :disagree
(and (<= 2.0 target) (< target 3.0)) :neutral
(and (<= 3.0 target) (< target 4.0)) :agree
(<= 4.0 target 5.0) :strongly-agree
(= target 42) :the-answer
:else :do-not-care)
Test cases:
(use '[clojure.test :only (deftest is run-tests)])
(deftest unit-tests
(letfn [(test-range-case [target]
(range-case target
[0.0 :ie 1.0] :greatly-disagree
[1.0 :ie 2.0] :disagree
[2.0 :ie 3.0] :neutral
[3.0 :ie 4.0] :agree
[4.0 :ii 5.0] :strongly-agree
42 :the-answer
:else :do-not-care))]
(is (= (test-range-case 0.0) :greatly-disagree))
(is (test-range-case 0.5) :greatly-disagree)
(is (test-range-case 1.0) :disagree)
(is (test-range-case 1.5) :disagree)
(is (test-range-case 2.0) :neutral)
(is (test-range-case 2.5) :neutral)
(is (test-range-case 3.0) :agree)
(is (test-range-case 3.5) :agree)
(is (test-range-case 4.0) :strongly-agree)
(is (test-range-case 4.5) :strongly-agree)
(is (test-range-case 5.0) :strongly-agree)
(is (test-range-case 42) :the-answer)
(is (test-range-case -1) :do-not-care)))
(run-tests)"
`(cond
~@(loop [cases cases ret []]
(cond
(empty? cases)
ret
(odd? (count cases))
(throw (IllegalArgumentException.
(str "no matching clause: " (first cases))))
(= :else (first cases))
(recur (drop 2 cases) (conj ret :else (second cases)))
(vector? (first cases))
(let [[lower-bound operator upper-bound] (first cases)
clause (second cases)
[condition clause]
(case operator
:ii `((<= ~lower-bound ~target ~upper-bound) ~clause)
:ie `((and (<= ~lower-bound ~target)
(< ~target ~upper-bound)) ~clause)
:ei `((and (< ~lower-bound ~target)
(<= ~target ~upper-bound)) ~clause)
:ee `((< ~lower-bound ~target ~upper-bound) ~clause)
(throw (IllegalArgumentException.
(str "unknown operator: " operator))))]
(recur (drop 2 cases) (conj ret condition clause)))
:else
(let [[condition clause]
`[(= ~target ~(first cases)) ~(second cases)]]
(recur (drop 2 cases) (conj ret condition clause)))))))
(defmacro range-case [target & cases]
"Compare the target against a set of ranges or constant values and return
the first one that matches. If none match, and there exists a case with the
value :else, return that target. Each range consists of a vector containing
one of the following patterns:
[upper-bound] if this is the first pattern, match any
target <= upper-bound
otherwise, match any target <= previous
upper-bound and <= upper-bound
[< upper-bound] if this is the first pattern, match any
target < upper-bound
otherwise, match any target <= previous
upper-bound and < upper-bound
[lower-bound upper-bound] match any target where lower-bound <= target
and target <= upper-bound
[< lower-bound upper-bound] match any target where lower-bound < target
and target <= upper-bound
[lower-bound < upper-bound] match any target where lower-bound <= target
and target < upper-bound
[< lower-bound < upper-bound] match any target where lower-bound < target
and target < upper-bound
Example:
(range-case target
[0 < 1] :strongly-disagree
[< 2] :disagree
[< 3] :neutral
[< 4] :agree
[5] :strongly-agree
42 :the-answer
:else :do-not-care)
expands to
(cond
(and (<= 0 target) (< target 1)) :strongly-disagree
(and (<= 1 target) (< target 2)) :disagree
(and (<= 2 target) (< target 3)) :neutral
(and (<= 3 target) (< target 4)) :agree
(<= 4 target 5) :strongly-agree
(= target 42) :the-answer
:else :do-not-care)
Test cases:
(use '[clojure.test :only (deftest is run-tests)])
(deftest unit-tests
(letfn [(test-range-case [target]
(range-case target
[0 < 1] :strongly-disagree
[< 2] :disagree
[< 3] :neutral
[< 4] :agree
[5] :strongly-agree
42 :the-answer
:else :do-not-care))]
(is (= (test-range-case 0) :strongly-disagree))
(is (= (test-range-case 0.5) :strongly-disagree))
(is (= (test-range-case 1) :disagree))
(is (= (test-range-case 1.5) :disagree))
(is (= (test-range-case 2) :neutral))
(is (= (test-range-case 2.5) :neutral))
(is (= (test-range-case 3) :agree))
(is (= (test-range-case 3.5) :agree))
(is (= (test-range-case 4) :strongly-agree))
(is (= (test-range-case 4.5) :strongly-agree))
(is (= (test-range-case 5) :strongly-agree))
(is (= (test-range-case 42) :the-answer))
(is (= (test-range-case -1) :do-not-care))))
(run-tests)"
(if (odd? (count cases))
(throw (IllegalArgumentException. (str "no matching clause: "
(first cases))))
`(cond
~@(loop [cases cases ret [] previous-upper-bound nil]
(cond
(empty? cases)
ret
(= :else (first cases))
(recur (drop 2 cases) (conj ret :else (second cases)) nil)
(vector? (first cases))
(let [condition (first cases)
clause (second cases)
[case-expr prev-upper-bound]
(let [length (count condition)]
(cond
(= length 1)
(let [upper-bound (first condition)]
[(if previous-upper-bound
`(and (<= ~previous-upper-bound ~target)
(<= ~target ~upper-bound))
`(<= ~target ~upper-bound))
upper-bound])
(= length 2)
(if (= '< (first condition))
(let [[_ upper-bound] condition]
[(if previous-upper-bound
`(and (<= ~previous-upper-bound ~target)
(< ~target ~upper-bound))
`(< ~target ~upper-bound))
upper-bound])
(let [[lower-bound upper-bound] condition]
[`(and (<= ~lower-bound ~target)
(<= ~target ~upper-bound))
upper-bound]))
(= length 3)
(cond
(= '< (first condition))
(let [[_ lower-bound upper-bound] condition]
[`(and (< ~lower-bound ~target)
(<= ~target ~upper-bound))
upper-bound])
(= '< (second condition))
(let [[lower-bound _ upper-bound] condition]
[`(and (<= ~lower-bound ~target)
(< ~target ~upper-bound))
upper-bound])
:else
(throw (IllegalArgumentException. (str "unknown pattern: "
condition))))
(and (= length 4)
(= '< (first condition))
(= '< (nth condition 3)))
(let [[_ lower-bound _ upper-bound] condition]
[`(and (< ~lower-bound ~target) (< ~target ~upper-bound))
upper-bound])
:else
(throw (IllegalArgumentException. (str "unknown pattern: "
condition)))))]
(recur (drop 2 cases)
(conj ret case-expr clause)
prev-upper-bound))
:else
(let [[condition clause]
`[(= ~target ~(first cases)) ~(second cases)]]
(recur (drop 2 cases) (conj ret condition clause) nil)))))))
In the book "The Scheme Programming Language, 4th Edition", by R. Kent Dybvig, on page 86, the author has written a define-syntax
(Scheme macro) for a case
statement that accepts ranges for its conditions. I thought I would try this in Clojure.
Here is the result.
How can I improve this? I use :ii
, :ie
, :ei
, and :ee
for the range operators, indicating inclusive-inclusive, inclusive-exclusive, exclusive-inclusive,
and exclusive-exclusive, respectively. Is there a better choice?
I chose to expand to a cond
rather than discrete if
statements because I felt that I would gain the benefit from any future improvements to the cond
macro.
(defmacro range-case [target & cases]
"Compare the target against a set of ranges or constant values and return
the first one that matches. If none match, and there exists a case with the
value :else, return that target. Each range consists of a vector containing
3 terms: a lower bound, an operator, and an upper bound. The operator must
be one of :ii, :ie, :ei, or :ee, which indicate that the range comparison
should be inclusive-inclusive, inclusive-exclusive, exclusive-inclusive,
or exclusive-exclusive, respectively.
Example:
(range-case target
[0.0 :ie 1.0] :greatly-disagree
[1.0 :ie 2.0] :disagree
[2.0 :ie 3.0] :neutral
[3.0 :ie 4.0] :agree
[4.0 :ii 5.0] :strongly-agree
42 :the-answer
:else :do-not-care)
expands to
(cond
(and (<= 0.0 target) (< target 1.0)) :greatly-disagree
(and (<= 1.0 target) (< target 2.0)) :disagree
(and (<= 2.0 target) (< target 3.0)) :neutral
(and (<= 3.0 target) (< target 4.0)) :agree
(<= 4.0 target 5.0) :strongly-agree
(= target 42) :the-answer
:else :do-not-care)
Test cases:
(use '[clojure.test :only (deftest is run-tests)])
(deftest unit-tests
(letfn [(test-range-case [target]
(range-case target
[0.0 :ie 1.0] :greatly-disagree
[1.0 :ie 2.0] :disagree
[2.0 :ie 3.0] :neutral
[3.0 :ie 4.0] :agree
[4.0 :ii 5.0] :strongly-agree
42 :the-answer
:else :do-not-care))]
(is (= (test-range-case 0.0) :greatly-disagree))
(is (test-range-case 0.5) :greatly-disagree)
(is (test-range-case 1.0) :disagree)
(is (test-range-case 1.5) :disagree)
(is (test-range-case 2.0) :neutral)
(is (test-range-case 2.5) :neutral)
(is (test-range-case 3.0) :agree)
(is (test-range-case 3.5) :agree)
(is (test-range-case 4.0) :strongly-agree)
(is (test-range-case 4.5) :strongly-agree)
(is (test-range-case 5.0) :strongly-agree)
(is (test-range-case 42) :the-answer)
(is (test-range-case -1) :do-not-care)))
(run-tests)"
`(cond
~@(loop [cases cases ret []]
(cond
(empty? cases)
ret
(odd? (count cases))
(throw (IllegalArgumentException.
(str "no matching clause: " (first cases))))
(= :else (first cases))
(recur (drop 2 cases) (conj ret :else (second cases)))
(vector? (first cases))
(let [[lower-bound operator upper-bound] (first cases)
clause (second cases)
[condition clause]
(case operator
:ii `((<= ~lower-bound ~target ~upper-bound) ~clause)
:ie `((and (<= ~lower-bound ~target)
(< ~target ~upper-bound)) ~clause)
:ei `((and (< ~lower-bound ~target)
(<= ~target ~upper-bound)) ~clause)
:ee `((< ~lower-bound ~target ~upper-bound) ~clause)
(throw (IllegalArgumentException.
(str "unknown operator: " operator))))]
(recur (drop 2 cases) (conj ret condition clause)))
:else
(let [[condition clause]
`[(= ~target ~(first cases)) ~(second cases)]]
(recur (drop 2 cases) (conj ret condition clause)))))))
UPDATE: Here is the revised version incorporating changes suggested by mikera and kotarak:
(defmacro range-case [target & cases]
"Compare the target against a set of ranges or constant values and return
the first one that matches. If none match, and there exists a case with the
value :else, return that target. Each range consists of a vector containing
one of the following patterns:
[upper-bound] if this is the first pattern, match any
target <= upper-bound
otherwise, match any target <= previous
upper-bound and <= upper-bound
[< upper-bound] if this is the first pattern, match any
target < upper-bound
otherwise, match any target <= previous
upper-bound and < upper-bound
[lower-bound upper-bound] match any target where lower-bound <= target
and target <= upper-bound
[< lower-bound upper-bound] match any target where lower-bound < target
and target <= upper-bound
[lower-bound < upper-bound] match any target where lower-bound <= target
and target < upper-bound
[< lower-bound < upper-bound] match any target where lower-bound < target
and target < upper-bound
Example:
(range-case target
[0 < 1] :strongly-disagree
[< 2] :disagree
[< 3] :neutral
[< 4] :agree
[5] :strongly-agree
42 :the-answer
:else :do-not-care)
expands to
(cond
(and (<= 0 target) (< target 1)) :strongly-disagree
(and (<= 1 target) (< target 2)) :disagree
(and (<= 2 target) (< target 3)) :neutral
(and (<= 3 target) (< target 4)) :agree
(<= 4 target 5) :strongly-agree
(= target 42) :the-answer
:else :do-not-care)
Test cases:
(use '[clojure.test :only (deftest is run-tests)])
(deftest unit-tests
(letfn [(test-range-case [target]
(range-case target
[0 < 1] :strongly-disagree
[< 2] :disagree
[< 3] :neutral
[< 4] :agree
[5] :strongly-agree
42 :the-answer
:else :do-not-care))]
(is (= (test-range-case 0) :strongly-disagree))
(is (= (test-range-case 0.5) :strongly-disagree))
(is (= (test-range-case 1) :disagree))
(is (= (test-range-case 1.5) :disagree))
(is (= (test-range-case 2) :neutral))
(is (= (test-range-case 2.5) :neutral))
(is (= (test-range-case 3) :agree))
(is (= (test-range-case 3.5) :agree))
(is (= (test-range-case 4) :strongly-agree))
(is (= (test-range-case 4.5) :strongly-agree))
(is (= (test-range-case 5) :strongly-agree))
(is (= (test-range-case 42) :the-answer))
(is (= (test-range-case -1) :do-not-care))))
(run-tests)"
(if (odd? (count cases))
(throw (IllegalArgumentException. (str "no matching clause: "
(first cases))))
`(cond
~@(loop [cases cases ret [] previous-upper-bound nil]
(cond
(empty? cases)
ret
(= :else (first cases))
(recur (drop 2 cases) (conj ret :else (second cases)) nil)
(vector? (first cases))
(let [condition (first cases)
clause (second cases)
[case-expr prev-upper-bound]
(let [length (count condition)]
(cond
(= length 1)
(let [upper-bound (first condition)]
[(if previous-upper-bound
`(and (<= ~previous-upper-bound ~target)
(<= ~target ~upper-bound))
`(<= ~target ~upper-bound))
upper-bound])
(= length 2)
(if (= '< (first condition))
(let [[_ upper-bound] condition]
[(if previous-upper-bound
`(and (<= ~previous-upper-bound ~target)
(< ~target ~upper-bound))
`(< ~target ~upper-bound))
upper-bound])
(let [[lower-bound upper-bound] condition]
[`(and (<= ~lower-bound ~target)
(<= ~target ~upper-bound))
upper-bound]))
(= length 3)
(cond
(= '< (first condition))
(let [[_ lower-bound upper-bound] condition]
[`(and (< ~lower-bound ~target)
(<= ~target ~upper-bound))
upper-bound])
(= '< (second condition))
(let [[lower-bound _ upper-bound] condition]
[`(and (<= ~lower-bound ~target)
(< ~target ~upper-bound))
upper-bound])
:else
(throw (IllegalArgumentException. (str "unknown pattern: "
condition))))
(and (= length 4)
(= '< (first condition))
(= '< (nth condition 3)))
(let [[_ lower-bound _ upper-bound] condition]
[`(and (< ~lower-bound ~target) (< ~target ~upper-bound))
upper-bound])
:else
(throw (IllegalArgumentException. (str "unknown pattern: "
condition)))))]
(recur (drop 2 cases)
(conj ret case-expr clause)
prev-upper-bound))
:else
(let [[condition clause]
`[(= ~target ~(first cases)) ~(second cases)]]
(recur (drop 2 cases) (conj ret condition clause) nil)))))))
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
我也会投票支持一些稍微冗长但读起来不那么难看的东西。
这可能是一个可行的替代方案。
I would also vote for something slightly more verbose but less ugly to read.
This could be a viable alternative.
一些想法:
另一种方法可能是让您的宏在案例级别工作,如下所示:
我个人喜欢这个,因为您可以如果需要,将范围测试与其他谓词混合。
Some ideas:
An alternative might be to make your macro work at the case level like follows:
I personally like this because you can mix your range tests with other predicates if needed.
我最初的看法:
这需要对语法进行轻微的更改,如下所示:
我的版本可能违反了原始示例的精神,但“优点”包括:
target< /代码>。您也不限于两个测试(较低的测试和较高的测试)。你可以做
[0 < x <= y < 4 <= z]
等。缺点?
x
重复了很多次。抓取x
并将其放在顶部是否值得增加复杂性和降低灵活性?话又说回来,此时我们的宏除了将方括号更改为括号并将一堆内容放在一起之外,并没有做更多的事情。所以我怀疑你是否真的需要一个宏。
My initial take on it:
This requires a slight change to syntax, like this:
My version may be violating the spirit of the original example, but "advantages" include:
target
. You also aren't limited to two tests (lower test and upper test). You could do[0 < x <= y < 4 <= z]
etc.Disadvantages?
x
is repeated a bunch of times. Is grabbingx
and putting it at the top worth the increase in complexity and decrease in flexibility?Then again, at this point our macro isn't doing much more than changing square brackets into parens and
and
ing a bunch of stuff together. So I question whether you really need a macro at all.