问题描述
|
在R. Kent Dybvig的书“ The Scheme Programming Language,第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)))))))
更新:这是修订版,其中包含了mikera和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,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)))))))
解决方法
我也会投票给我一些冗长但丑陋的东西。
(range-case target
[(<= 0.0) (< 1.0)] :greatly-disagree
[(<= 1.0) (< 2.0)] :disagree
[(<= 2.0) (< 3.0)] :neutral
[(<= 3.0) (< 4.0)] :agree
(<= 4.0 5.0) :strongly-agree
42 :the-answer
:else :do-not-care)
这可能是一个可行的选择。
, 一些想法:
具有运算符的默认值(例如,“:ie \”在典型问题中可能是最自然的)
将一个边界默认设置为上一个或下一个上/下边界,这样您就无需重复相同的边界值。
考虑ifs而不是cond,以便您可以进行间隔二等分(如果您期望大量案例,那将是性能上的胜利)
一种替代方法是使宏在案例级别下工作,如下所示:
(cond
(in-range target [0.0 1.0]) :greatly-disagree)
(in-range target [1.0 2.0]) :disagree)
...)
我个人喜欢这样,因为如果需要,您可以将范围测试与其他谓词混合。
, 我最初的看法:
(defn make-case [test val]
(if (vector? test)
`((and ~@(for [[lower comp upper] (partition 3 2 test)]
(list comp lower upper)))
~val)
(list :else val)))
(defmacro range-case [& cases]
(let [cases (partition 2 cases)]
`(cond ~@(mapcat (partial apply make-case) cases))))
这需要对语法进行一些更改,如下所示:
(range-case
[0.0 <= x < 1.0] :greatly-disagree
[1.0 <= x < 2.0] :disagree
[2.0 <= x < 3.0] :neutral
[3.0 <= x < 4.0] :agree
[4.0 <= x <= 5.0] :strongly-agree
[42 = x] :the-answer
:else :do-not-care)
我的版本可能违反了原始示例的精神,但是“优点”包括:
您不会硬编码为一个15英镑。您也不限于两个测试(较低的测试和较高的测试)。你可以做[0 < x <= y < 4 <= z]
等。
语法更类似于数学比较符号。
Clojure的比较运算符可以自己作为参数传递。无需使用关键字并将其转换为比较运算符。这样,比较运算符就不会硬编码到函数中,而删除此间接层会使它的读取效果更好。
平等不再是特例。
劣势?
x
被重复了很多次。抓住ѭ17并将其放在顶部是否值得增加复杂性并降低灵活性?
像您的原始示例一样,它使用中缀表示法。在前缀表示法的世界中,这可能会有些刺耳。
再说一遍,在这一点上,我们的宏所做的不只是将方括号更改为括号并“ 19”将一堆东西放在一起。所以我质疑您是否真的需要一个宏。
(defn ?? [& xs]
(every? (fn [[lower comp upper]]
(comp lower upper))
(partition 3 2 xs)))
(cond
(?? 0.0 <= x < 1.0) :greatly-disagree
(?? 1.0 <= x < 2.0) :disagree
(?? 2.0 <= x < 3.0) :neutral
(?? 3.0 <= x < 4.0) :agree
(?? 4.0 <= x <= 5.0) :strongly-agree
(= 42 x) :the-answer
:else :do-not-care)