Clojure是否存在诸如CLOS(通用 Lisp 对象系统)之类的东西?
7 回答
Clojure 本身没有对象系统,原因有两个:
- Clojure 专门设计用于托管在面向对象的平台上,然后它简单地吸收了底层平台的对象系统。即ClojureJVM有JVM对象系统,ClojureCLR有CLI对象系统,ClojureScript有ECMAScript对象系统,以此类推。
- Rich Hickey 讨厌物体。
但是,您显然可以在 Clojure 中实现一个对象系统。毕竟,Clojure 是图灵完备的。
Mikel Evins正在研究一种新的面向对象的方法,他称之为类别。他有几个 Lisps 的实现,包括 Clojure(尽管并非所有端口都保证始终是最新的)。
类别正在慢慢被Bard所包含,这是 Mikel 正在设计的一种新的 Lisp 方言,它内置了类别。(然后,这反过来可能成为Closos的实现语言,这是 Mikel 关于如何设计操作系统的一个想法。 )
Clojure 没有 CLOS,也不需要 CLOS,但您可以实现它。
Clojure 想要是不可变的,所以拥有可变的 OO 有点愚蠢,但你可以拥有一种 OO。
- http://clojure.org/datatypes(查看 defrecord --> 最好的类和哈希映射)
- http://clojure.org/protocols(有点像接口但更好)
- http://clojure.org/multimethods(功能强大,因为您可以编写自己的调度函数)
有了这三件事,您应该能够满足您的所有需求,但大多数时候,最好只使用普通函数和标准数据结构。
使用 OO 范式非常适合编写松散耦合的代码、模拟和测试。Clojure 使这很容易实现。
我过去遇到的一个问题是代码依赖于其他代码。如果使用不当,Clojure 命名空间实际上会加剧问题。理想情况下,可以模拟命名空间,但正如我发现的那样......模拟命名空间存在很多问题:
https://groups.google.com/forum/?fromgroups=#!topic/clojure/q3PazKoRlKU
一旦您开始构建越来越大的应用程序,命名空间就会开始相互依赖,并且在没有大量依赖项的情况下单独测试您的高级组件真的很不方便。大多数解决方案都涉及函数重新绑定和其他黑魔法,但问题是到了测试时间,原始依赖项仍在加载 -> 如果您有一个大型应用程序,这将成为大问题。
使用数据库后,我有动力寻找替代方案。数据库库给我带来了很多痛苦——它们需要很长时间才能加载,并且通常是应用程序的核心。如果不将整个数据库、库和相关外围设备带入您的测试代码,就很难测试您的应用程序。
您希望能够打包您的文件,以便可以“换出”依赖于数据库代码的系统部分。OO 设计方法提供了答案。
很抱歉,答案很长......我想给出一个很好的理由来解释为什么使用 OO 设计而不是如何使用它。所以必须使用一个实际的例子。我试图保留ns
声明,以便示例应用程序的结构尽可能清晰。
现有的 clojure 样式代码
此示例使用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}]
用 clojure 模拟 - 'OO' 风格
所以这样做的真正好处是该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 可以使用其多方法系统支持对分层定义的类型进行调度。基本功能是defmulti和defmethod。(也许这些在第一次回答问题时不可用。)
CLOS 相对不寻常的特性之一是它支持在多个参数类型上分派的函数。Clojure 非常自然地模拟了这种行为,正如这里的一个例子所暗示的那样。(该示例本身不使用类型——但这是 Clojure 多方法灵活性的一部分。与此处的第一个示例进行比较。)
CljOS是 Clojure 的玩具 OOP 库。完全没有这个词的意义。只是我为了好玩而做的东西。
这是一个旧帖子,但我想回应它。
没有 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 也会有很大的不同。大多数标准设计模式和构造都考虑到了可变性。