在 Clojure 的宏中声明设计模式

Posted

技术标签:

【中文标题】在 Clojure 的宏中声明设计模式【英文标题】:Declaring design pattern in a Macro in Clojure 【发布时间】:2011-07-11 22:14:21 【问题描述】:

Lisp 的优点之一是宏。我一直在阅读很多关于你在 Java 中一次又一次地编写设计模式的文章。不在 Lisp/Clojure 中。 在 Lisp/Clojure 中,您可以在宏中声明模式,并且只需要编写实际代码。

好的,不错,但眼见为实。

能否请您提供(或推荐给我)一个带有代码的示例 - 最好是 Clojure - 关于如何在宏中声明设计模式?

【问题讨论】:

宏(除其他外)有助于删除“样板代码”。通常,您会发现代码中一遍又一遍地重复的“模式”。通常,您可以通过使用闭包将模式封装在函数中,但有时您希望使其看起来更平滑,然后使用宏来添加额外的语法糖。 【参考方案1】:

到目前为止所有好的答案。我想强调一下,宏和函数之间的主要实际区别在于,宏可以控制何时以及是否评估其参数,而函数不能。除其他含义外,这还允许您使用该语言编写新的控件构造,而这是函数无法做到的。

例如,看看来自 Clojure 核心的the if-not macro。这与标准的 "if" 完全一样,但相反:如果条件为假,则运行代码的第一位;如果条件为真,则运行第二个。

如果你把类似的东西写成函数,它就行不通。函数版本将在调用函数时立即运行“then”和“else”代码,无论条件是真还是假。另一方面,宏版本可以选择是否(以及何时)运行作为参数给出的代码。

一个不那么琐碎的例子是abstracting out the typical "try ... catch ... finally" control construct 在 Java 中无处不在,在其他语言中也很常见,它变成了一个“with-whatever”宏。如果您持有必须释放的有限资源(例如文件句柄或网络套接字),即使出现错误,您也需要一个“finally”块来执行此操作,但您的应用程序代码必须在未计算的情况下拼接到“try”块的内部,在该上下文中运行。

您最终复制了基本上相同的未更改样板 try...catch...finally 在您的程序中到处阻塞,并将一个小的本地适用部分粘贴到“try”部分。 (查看任何重要的 Java 源代码。)这个样板不能抽象成函数,因为函数会在调用时立即在调用者的上下文中评估本地代码,然后给出 result 将该代码添加到“with-whatever”函数中。

另一方面,宏可以延迟评估,直到宏的代码特别调用,允许它将您的任意本地代码拼接到未评估的“try”构造的内部。然后将整个结果(整个 try/catch/finally,包括 try 中未评估的特定于应用程序的代码)返回给调用者并拼接到调用者的上下文中,完成后在那里执行。

因此,您可以编写一个编写程序的程序。

【讨论】:

【参考方案2】:

这不是关于宏,而是关于函数式编程。宏可以使东西看起来和感觉更好,但它都是关于功能的。如果你用 FP 编写,你就不会遇到很多你必须一直重复的习语(至少没有一个你不能用更多的功能做得更好)

采取战略模式。如果你的语言有 lambda,你就不再需要它了。

您可能经常使用相同的模式,但您的语言使其表达起来非常容易,以至于您永远不会将其称为实现设计模式。这只是编程。

【讨论】:

您完全误解了设计模式的意图。模式是简明描述常见编程习语的词典。 STRATEGY 仍然存在于函数式编程中,只是更简洁。 Aino Curry 在infoq.com/presentations/Functional-Design-Patterns 中展示了 23 种 GoF 模式中的 16 种基本上是“一流公民” 为了更清楚,想象一下 FLIGHT 是我们应用于人类的模式名称,用于人类在空中。我们可能会使用飞机来实现 FLIGHT。你基本上是在说“如果人类有翅膀,我们就不需要飞行,因为我们不用飞机。”这是不正确的。 FLIGHT 的实现细节可能会因范式而异,但该模式的目的是传达更高层次的想法。【参考方案3】:

实现设计模式的典型宏用法示例是应用于现有函数的“装饰器”模式。

; a simple function
(defn square [x] (* x x))

; a macro to "decorate" a function with a debug output println
(defmacro with-debug-output [f] 
  `(fn [& args#] 
     (let [result# (apply ~f args#)]
       (println (str "Debug-output: " result#))
       result#)))


; call the straight function
(square 16)
=> 256

; call the decorated function
((with-debug-output square) 16)
Debug-output: 256
=> 256

注意:您实际上并不需要宏来执行此操作,您也可以使用高阶函数来执行此操作。

【讨论】:

为此使用宏对我来说似乎是个糟糕的主意。一个函数给你同样多的好处,而且没有任何奇怪的陷阱。例如,您可以使用函数(let [functions [...], debug-versions (map wrap-debug-output functions)] ...) 仅供参考,这本质上是the "spy" macro from Clojure itself。 (我仍然不确定为什么这是一个宏而不是函数,正如 amalloy 所提到的,因为它不需要在某些情况下不评估其参数。)【参考方案4】:

大多数现有的设计模式都起源于面向对象的世界,并且仅在面向对象的世界中才有意义。一旦你进入函数式编程,尤其是像 Clojure 这样的 Lisp 方言,你对设计模式的需求就会变得越来越小。有一个关于设计模式和 FP here 的有趣讨论。

另一方面,宏并非旨在封装设计模式,而是通过更方便地解决手头问题的结构来扩展语言。以 with-open 宏为例:将其称为设计模式以在资源上调用 close 似乎是完全错误的。

模式也存在于 FP 世界中,但由于您不再拥有对象,因此它们的主要重点是算法。 FP 语言的“模式”很好的例子是monads 和zippers。

警告:理解这些概念可能需要一些时间,但绝对值得理解它们的每一点。

【讨论】:

我不同意宏的唯一目的是用新的结构扩展语言。它们的一个更重要的应用是 eDSL 的实现。 eDSL 可以代表某些设计模式。但我同意,如果您经常遇到重复的模式,那么您的语言根本就不够表达。 我认为嵌入式 DSL语言的扩展,将包含在 skuro 的含义中。

以上是关于在 Clojure 的宏中声明设计模式的主要内容,如果未能解决你的问题,请参考以下文章

在 C 中的宏中替换宏

如果比较列表,Clojure的deftest宏中的断言错误

从 Base (Libreoffice) 中的宏中获取按钮

使用以 10.0.0 形式定义版本号的宏

是否可以在 Rust 的过程宏中存储​​状态?

Common Lisp宏中的词法绑定