2

我正在阅读优秀的书 Let Over Lambda,我正在尝试将用于定义单元的 Common Lisp 代码移植到 Clojure。

以下生成一个应该采用的宏

(defn defunits-chaining [u units prev]
  (if (some #(= u %) prev)
    (throw (Throwable. (str u " depends on " prev))))
  (let [spec (first (filter #(= u (first %)) units))]
    (if (nil? spec)
      (throw (Throwable. (str "unknown unit " u)))
      (let [chain (second spec)]
        (if (list? chain)
          (* (first chain)
             (defunits-chaining
               (second chain)
               units
               (cons u prev)))
          chain)))))

(defmacro defunits [quantity base-unit & units]
  `(defmacro ~(symbol (str "unit-of-" quantity))
     [valu# un#]
     `(* ~valu#
         ~(case un#
           ~base-unit 1
            ~@(map (fn [x]
                     `(~(first x)               ;; <-- PRETTY SURE IT'S THIS `(
                       ~(defunits-chaining
                          (first x)
                          (cons `(~base-unit 1)
                                (partition 2 units))
                          nil)))
                   (partition 2 units))))))

(defunits time s m 60 h 3600)

并将其变成一个可以调用的宏

(unit-of-time 4 h)

并以基本单位(在这种情况下为秒)给出结果。我认为问题是“案例”中的 Clojure/CL api 更改。CL 中的“案例”如下所示:

(case 'a (('b) 'no) (('c) 'nope) (('a) 'yes))

但是在clojure中...

(case 'a 'b 'no 'c 'nope 'a 'yes)

多么方便。我在嵌套的 defmacro 中更改了我的匿名函数,但它不断生成

(case un#
  s 1
  (m 60)
  (h 3600)

我怎样才能防止那些外部括号?

4

3 回答 3

6

如果您将地图包裹在一个平面中,它应该会产生您正在寻找的结果。

(defmacro defunits [quantity base-unit & units]
  `(defmacro ~(symbol (str "unit-of-" quantity))
     [valu# un#]
     `(* ~valu#
         ~(case un#
           ~base-unit 1
            ~@(flatten ;; <- changed this
                (map (fn [x]
                       `(~(first x)
                         ~(defunits-chaining
                            (first x)
                            (cons `(~base-unit 1)
                                  (partition 2 units))
                            nil)))
                   (partition 2 units)))))))

发生了什么:在您的初始实现中,当您需要的是原子列表时,您的地图会返回列表列表。 Flatten采用任意深度的列表并将其转换为单个值列表。

另一种方法是使用reduce而不是 map:

(defmacro defunits [quantity base-unit & units]
  `(defmacro ~(symbol (str "my-unit-of-" quantity))
     [valu# un#]
     `(* ~valu#
         ~(case un#
           ~base-unit 1
            ~@(reduce (fn [x y] ;; <- reduce instead of map
                        (concat x ;; <- use concat to string the values together
                              `(~(first y)
                                ~(defunits-chaining
                                   (first y)
                                   (cons `(~base-unit 1)
                                         (partition 2 units))
                                   nil))))
                      '()
                      (partition 2 units))))))

这将避免首先创建列表列表,因为 reduce 会将所有传入的值汇总到一个结果中。

更好的是,(感谢amalloy发现了这个),还有mapcat

(defmacro defunits [quantity base-unit & units]
  `(defmacro ~(symbol (str "unit-of-" quantity))
     [valu# un#]
     `(* ~valu#
         ~(case un#
            ~base-unit 1
            ~@(mapcat (fn [x] ;; <----- mapcat instead of map is the only change
                        `(~(first x)
                          ~(defunits-chaining
                             (first x)
                             (cons `(~base-unit 1)
                                   (partition 2 units))
                             nil)))
                      (partition 2 units))))))

Mapcat 有效地做与 reduce 版本相同的事情,但隐式地为您处理 concat。

于 2013-05-03T01:31:28.937 回答
4

只需将您更改mapmapcat.

(defmacro defunits [quantity base-unit & units]
  `(defmacro ~(symbol (str "unit-of-" quantity))
     [valu# un#]
     `(* ~valu#
         ~(case un#
            ~base-unit 1
            ~@(mapcat (fn [x] ;; <----- mapcat instead of map is the only change
                        `(~(first x)
                          ~(defunits-chaining
                             (first x)
                             (cons `(~base-unit 1)
                                   (partition 2 units))
                             nil)))
                      (partition 2 units))))))

详细说明并回应当前接受的答案:flatten永远、永远、永远都不正确(只有少数例外情况仅供专家使用)。充其量它是一种创可贴,可以为简单的输入提供正确的结果,但在复杂的输入中失败。相反,通常使用apply concat将列表扁平化一层,或者使用mapcat而不是map生成已经扁平的列表,或者可能更复杂和专门用于特别复杂的数据结构的东西是正确的。在这里,只需要一个简单mapcat的。

我想我应该注意到defunits宏的输入受到如此严格的限制(它们实际上必须是符号和数字),实际上不会有任何输入flatten会产生错误的输出。但是扁平化是一个不好的习惯,并且无论如何都会导致更长,更复杂的代码。

于 2013-05-03T03:39:00.003 回答
3

您可以连接带括号的对(使用,比如说,apply concat)或使用 amalloy 的答案(最好的主意)。

您还可以使用 seq 函数构建您的退货表格:

(defmacro defunits [...]
  (let [macro-name (symbol ...)
        valsym (gensym "val__")
        unitsym (gensym "unit__")]
    (list `defmacro             ; using ` so the symbol gets ns-qualified
          macro-name
          [valsym unitsym]
          (make-body-expression ...)))) ; define appropriately

我认为这在这里是有道理的,因为无论如何你最终都会取消引用所有内容,有两个级别的语法引用;不过,这在很大程度上是一个品味问题。

于 2013-05-03T01:12:36.587 回答