【问题标题】:Backquote without parens没有括号的反引号
【发布时间】:2013-05-03 00:57:19
【问题描述】:

我正在阅读优秀的书 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)

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

【问题讨论】:

    标签: macros clojure common-lisp


    【解决方案1】:

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

    (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 而不是地图:

    (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。

    【讨论】:

    • 是的,~@(flatten (map (fn [x] 做得很好,我不知道在我尝试的一百件事中我是怎么错过的。head smack
    【解决方案2】:

    只需将您的 map 更改为 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))))))
    

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

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

    【讨论】:

    • 这正是我之前所寻找的。我想我需要在 clojure 标准函数中进行一些挖掘。
    • 对,再看问题,这是正确答案。
    【解决方案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
    

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

    【讨论】:

    • 哇,非常感谢,这需要我一两天的时间来解决。我不知道你可以从 seq 构造它,这真的很酷。这是经过深思熟虑的,非常感谢。
    • 宏只是适当注册(使用编译器)返回列表结构的函数。带取消引号的反引号/语法引用只是产生列表结构的一种可能方式。每个都可以单独使用(并且有用)(尽管我相信在 Clojure 中,我发现自己在宏之外使用语法引用的频率低于在 Scheme 中使用反引号的频率)。
    • 编辑表明我认为 amalloy 的答案是最值得打勾的。我仍然更喜欢展开一层语法引用,所以我会为这个建议保留这个答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-26
    • 1970-01-01
    • 1970-01-01
    • 2021-08-07
    • 2021-12-29
    • 2012-01-18
    相关资源
    最近更新 更多