【发布时间】:2011-05-02 01:47:24
【问题描述】:
Clojure 是否存在类似CLOS(通用 Lisp 对象系统)的东西?
【问题讨论】:
标签: oop clojure lisp common-lisp clos
Clojure 是否存在类似CLOS(通用 Lisp 对象系统)的东西?
【问题讨论】:
标签: oop clojure lisp common-lisp clos
您是否考虑过 Clojure 的 data types(尤其是 defrecord)、protocols 和 multimethods?这三个在 Clojure 中总是比在这些机制之上的 CLOS 端口更惯用。
【讨论】:
eval,我们可能从来没有在计算机上安装过 Lisp。从此开始了 Lisp 黑客的漫长历史,他们不尊重别人所说的正确做法!所以我说,去吧,将 CLOS 移植到 Clojure,看看会发生什么。真的,可能发生的最坏情况是什么? :-)
Clojure 没有 CLOS,也不需要 CLOS,但您可以实现它。
Clojure 想要是不可变的,所以拥有可变的 OO 有点愚蠢,但你可以拥有一种 OO。
有了这三样东西,你应该可以满足你的所有需求,但大多数时候,最好只使用普通函数和标准数据结构。
【讨论】:
Clojure 本身没有对象系统,原因有两个:
但是,您可以显然在 Clojure 中实现一个对象系统。毕竟,Clojure 是图灵完备的。
Mikel Evins 正在研究一种新的面向对象的方法,他称之为Categories。他有几个 Lisps 的实现,包括 Clojure(尽管并非所有端口都保证始终是最新的)。
Categories 正慢慢被 Bard 所包含,这是 Mikel 正在设计的一种新的 Lisp 方言,它内置了 Categories。(然后,这可能会成为 Closos 的实现语言,这是 Mikel 的想法如何设计操作系统。)
【讨论】:
这是一个旧帖子,但我想回复它。
没有 clojure 没有 OO 支持,也没有 CLOS 支持。环境的底层对象系统仅在互操作性方面几乎不可用,而不是用于在 clojure 中创建自己的类/对象层次结构。 Clojure 旨在轻松访问 CLR 或 JVM 库,但 OOP 支持到此结束。
Clojure 是一个 lisp 并支持闭包和宏。考虑到这 2 个特性,您可以用几行代码开发一个基本的对象系统。
现在的重点是你真的需要 lisp 方言中的 OOP 吗?我会说不,是的。不,因为大多数问题都可以在没有对象系统的情况下解决,并且在任何 lisp 中都可以更优雅地解决。我会说是的,因为您仍然会不时需要 OOP,因此最好提供一个标准的参考实现,而不是让每个极客都实现它。
我建议您看看 Paul Graham 的 On Lisp 书。您可以在线免费查阅。
这真是一本好书,真正掌握了lisp的精髓。您必须稍微调整语法以适应 clojure,但概念保持不变。对您的问题很重要,最后一章中的一章展示了如何在 lisp 中定义自己的对象系统。
顺便说一句,clojure 包含不变性。你可以在 clojure 中创建一个可变的对象系统,但是如果你坚持不变性,你设计,即使使用 OOP 也会有很大的不同。大多数标准设计模式和构造都考虑到了可变性。
【讨论】:
使用 OO 范式非常适合编写松散耦合的代码、模拟和测试。 Clojure 让这一切变得如此容易。
我过去遇到的一个问题是代码依赖于其他代码。如果使用不当,Clojure 命名空间实际上会加剧问题。理想情况下,可以模拟命名空间,但正如我发现的那样......模拟命名空间存在很多问题:
https://groups.google.com/forum/?fromgroups=#!topic/clojure/q3PazKoRlKU
一旦您开始构建越来越大的应用程序,命名空间就会开始相互依赖,并且在没有大量依赖项的情况下单独测试您的更高级别的组件会变得非常不合适。大多数解决方案都涉及函数重新绑定和其他黑魔法,但问题是到了测试时间,原始依赖项仍在加载 -> 如果您有一个大型应用程序,这将成为大问题。
在使用数据库库后,我有动力去寻找替代方案。数据库库给我带来了很多痛苦——它们需要很长时间才能加载,而且通常是应用程序的核心。如果不将整个数据库、库和相关外围设备带入您的测试代码,就很难测试您的应用程序。
您希望能够打包您的文件,以便可以“换出”依赖于您的数据库代码的系统部分。 OO 设计方法提供了答案。
很抱歉,答案很长...我想就为什么使用OO设计而不是如何使用它提供一个很好的理由。所以必须使用一个实际的例子。我已尝试保留 ns 声明,以便示例应用程序的结构尽可能清晰。
本例使用carmine,这是一个redis客户端。与 korma 和 datomic 相比,它使用起来相对容易并且启动速度很快,但是数据库库仍然是数据库库:
(ns redis-ex.history
(:require [taoensso.carmine :as car]
[clojure.string :as st]))
(defmacro wcr [store kdir f & args]
`(car/with-conn (:pool ~store) (:conn ~store)
(~f (st/join "/" (concat [(:ns ~store)] ~kdir)) ~@args)))
(defn empty [store kdir]
(wcr store kdir car/del))
(defn add-instance [store kdir dt data]
(wcr store kdir car/zadd dt data))
(defn get-interval [store kdir dt0 dt1]
(wcr store kdir car/zrangebyscore dt0 dt1))
(defn get-last [store kdir number]
(wcr store kdir car/zrange (- number) -1))
(defn make-store [pool conn ns]
{:pool pool
:conn conn
:ns ns})
所有的功能都应该被测试...这不是什么新鲜事,是标准的clojure代码
(ns redis-ex.test-history0
(:require [taoensso.carmine :as car]
[redis-ex.history :as hist]))
(def store
(hist/make-store
(car/make-conn-pool)
(car/make-conn-spec)
"test"))
(hist/add-instance store ["hello"] 100 100) ;;=> 1
(hist/get-interval store ["hello"] 0 200) ;;=> [100]
在观看 Misko Hevery 的演讲后,我想到了“OO”并不邪恶但实际上非常有用的想法:
http://www.youtube.com/watch?v=XcT4yYu_TTs
基本思想是,如果您想构建一个大型应用程序,您必须将“功能”(程序的核心)与“接线”(接口和依赖项)分开。依赖越少越好。
我使用 clojure 哈希映射作为“对象”,因为它们没有库依赖关系并且是完全通用的(请参阅 Brian Marick 谈论在 ruby 中使用相同的范例 - http://vimeo.com/34522837)。
要使您的 clojure 代码“面向对象”,您需要以下函数 - (send 从 smalltalk 窃取) 如果它与现有键相关联,它只会调度与映射中的键相关联的函数。
(defn call-if-not-nil [f & vs]
(if-not (nil? f) (apply f vs))
(defn send [obj kw & args]
(call-if-not-nil (obj kw) obj))
我在通用实用程序库中提供了实现(hara.fn 命名空间中的 https://github.com/zcaudate/hara)。如果您想自己实现,只需 4 行代码。
您现在可以修改原来的make-store 函数以在地图中添加函数。现在您有了一定程度的间接性。
;;; in the redis-ex.history namespace, make change `make-store`
;;; to add our tested function definitions as map values.
(defn make-store [pool conn ns]
{:pool pool
:conn conn
:ns ns
:empty empty
:add-instance add-instance
:get-interval get-interval
:get-last get-last})
;;; in a seperate test file, you can now test the 'OO' implementation
(ns redis-ex.test-history1
(:require [taoensso.carmine :as car]
[redis-ex.history :as hist]))
(def store
(hist/make-store
(car/make-conn-pool)
(car/make-conn-spec)
"test"))
(require '[hara.fn :as f])
(f/send store :empty ["test"])
;; => 1
(f/send store :get-instance ["test"] 100000)
;; => nil
(f/send store :add-instance ["test"]
{100000 {:timestamp 1000000 :data 23.4}
200000 {:timestamp 2000000 :data 33.4}
300000 {:timestamp 3000000 :data 43.4}
400000 {:timestamp 4000000 :data 53.4}
500000 {:timestamp 5000000 :data 63.4}})
;; => [1 1 1 1 1]
所以因为make-store 函数构造了一个完全自包含的store 对象,所以可以定义函数来利用这一点
(ns redis-ex.app
(:require [hara.fn :as f]))
(defn get-last-3-elements [st kdir]
(f/send st :get-last kdir 3))
如果你想使用它......你会做这样的事情:
(ns redis-ex.test-app0
(:use redis-ex.app
redis-ex.history)
(:require [taoensso.carmine :as car]))
(def store
(hist/make-store
(car/make-conn-pool)
(car/make-conn-spec)
"test"))
(get-last-3-elements ["test"] store)
;;=> [{:timestamp 3000000 :data 43.4} {:timestamp 4000000 :data 53.4} {:timestamp 5000000 :data 63.4}]
所以这样做的真正优势在于get-last-3-elements 方法可以位于完全不同的命名空间中。它根本不依赖于数据库实现,因此现在测试这个功能只需要一个轻量级的工具。
模拟然后定义是微不足道的。无需加载任何数据库即可完成 redis-ex.usecase 命名空间的测试。
(ns redis-ex.test-app1
(:use redis-ex.app))
(defn make-mock-store []
{:database [{:timestamp 5000000 :data 63.4}
{:timestamp 4000000 :data 53.4}
{:timestamp 3000000 :data 43.4}
{:timestamp 2000000 :data 33.4}
{:timestamp 1000000 :data 23.4}]
:get-last (fn [store kdir number]
(->> (:database store)
(take number)
reverse))})
(def mock-store (make-mock-store))
(get-last-3-elements ["test"] mock-store)
;; => [{:timestamp 3000000 :data 43.4} {:timestamp 4000000 :data 53.4} {:timestamp 5000000 :data 63.4}]
【讨论】:
较早的帖子将这个问题作为一个关于在 Clojure 中实现对面向对象编程的各种特性的特定支持的价值和可能性的问题。但是,有一系列与该术语相关的属性。并非所有面向对象的语言都支持所有这些。而且 Clojure 直接支持其中一些属性,无论您是否想称其支持“面向对象”。我会提到其中的几个属性。
Clojure 可以使用其多方法系统支持dispatch on hierarchically defined types。基本功能为defmulti 和defmethod。 (也许这些在第一次回答问题时不可用。)
CLOS 相对不寻常的特性之一是它支持对多个参数类型进行分派的函数。 Clojure 非常自然地模拟了这种行为,例如 here 所建议的。 (该示例本身并未使用类型——但这是 Clojure 多方法灵活性的一部分。与第一个示例 here 进行比较。)
【讨论】:
CljOS 是 Clojure 的玩具 OOP 库。完全没有这个词的意义。只是为了好玩。
【讨论】: