Tiny CLOS教程

By guenchi at 2018-09-10 • 0人收藏 • 112人看过

https://home.adelphi.edu/sbloch/class/archive/272/spring1997/tclos/tutorial.html

Tiny CLOS教程

我假设你已经相当熟悉Scheme语言; 如果没有,请先阅读Scheme Tutorial 。

你现在回来了吗? 好。

目录


介绍

CLOS是Common Lisp Object System的首字母缩写,是Common Lisp语言的一组标准扩展,用于帮助人们在Lisp中进行面向对象的编程。 如你所知,Scheme是一种Lisp方言,其语法比Common Lisp更简单,更一致。 Tiny CLOS是Gregor Kiczales于1992年编写的CLOS的Scheme版本。 它在语法上与CLOS不同,但OOP的基本方法与CLOS相同。

如何使用微小的CLOS

Tiny CLOS是用Scheme编写的。 如果你真的想看它,源代码在文件tiny-clos.scm中 ,但由于需要引导对象和类系统,它有很多复杂性; 一个更容易理解(但不是功能)的版本是在tiny-rpp.text中 。 其他有用的支持代码位于此目录的其余部分。

但是,您不需要使用源代码:更方便的是使用命令tclos ,这是一个运行MIT Scheme的简单shell脚本,其内存映像已经加载了Tiny CLOS。 如果你不知道这意味着什么,请忽略它; 只需使用命令tclos。

CLOS 其他OOP方法

当今最流行的面向对象语言( 例如 C ++和Java)共享其语法和大部分哲学。 CLOS和Tiny CLOS具有类似Lisp的语法,与上面提到的其他语言的块结构语法非常不同。 它们与基本上所有其他OO语言共享运行时多态性的概念(  ,根据应用它的对象的种类以几种不同的方式工作的函数),继承 。 大多数 OO语言一样,CLOS和Tiny CLOS认为每个“对象”都是“类”的元素,可以用一个或多个“超类”来编写。

CLOS与上面提到的其他语言之间最显着的区别在于,在CLOS(和Tiny CLOS)中,多态函数的外观和行为类似于普通函数,不依赖于任何一类对象。 相比之下,C ++,Java 等中的每个多态函数都“属于”一个特定的类,并且必须与该类的实例一起调用。 例如,假设有一个名为dial的类和一个名为turn的多态函数,我们希望将拨号ThisDial设置为200.一个C ++程序员会写 
ThisDial.turn (200); 
而CLOS程序员会写 
(turn ThisDial 200) 
区别不仅仅是语法:在C ++中,涉及多个对象的动作必须“属于”其中一个,其余的作为参数。 再举一个例子,假设有两个名为LightBulb和socket类,我们希望编写一个多态函数(除其他外)将灯泡放入套接字。 在C ++(或Java或...)中,程序员必须选择是否为灯泡编写方法,将套接字作为参数或套接字的方法,将灯泡作为参数: 
ThisBulb.PutIn (ThatSocket); ThatSocket.PutIn (ThisBulb); 
而CLOS程序员只是简单地写道 
(PutIn ThisBulb ThatSocket) 
实际上,多态函数(在CLOS术语中称为“泛型函数”)是具有多个定义的普通函数,它在运行时根据其参数的类自动选择最合适的定义。 没有任何一个参数被挑选出来作为该方法适用的“对象”,并且几乎不需要称为“朋友”的C ++构造,这是一个应用于一个类的对象的函数,但是它仍然可以访问一个类的私有信息。另一类的对象。

如上所述,这种选择有几个优点。 它也有缺点:由于方法不属于某个特定类,而属于类的组合,因此控制方法的可见性要困难得多,并且C ++中的public/protected/private区别不能应用于方法。 你是否认为这些缺点超过了优点,是个人的,几乎是宗教的决定。 有关此问题的更多讨论,请参阅OOP常见问题解答第1部分 ,第1.19项。


CLOS中的类和对象

在CLOS中,与大多数面向对象语言一样,每个“对象”都是一个或多个“类”的元素,其定义可以从其他“超类”的定义派生。 对象的行为由其类确定:这里“行为”指的是

  • 实例变量, 与类中每个对象关联的信息

  • 方法, 应用于类中的对象时如何实现各种函数

  • 可能还有其他类似于类或池变量的东西,我们现在还没有讨论过

按照惯例,CLOS中的类名称用尖括号括起来, 例如 <object>, <person> 。

创建实例

要创建现有类的新实例,请使用make函数:

(define sam (make <person>))

创建<person>类的新实例,并将Scheme变量sam绑定到它。 某些类以这种方式定义(参见下面的初始化函数),可以向make函数提供其他参数以确定新实例的属性, 例如初始化其实例变量。 例如,可以以这样的方式定义<person>类 
(define sam (make <person> 'age 38)) 
会将名为age的新<person>的实例变量初始化为38。


创建类

创建一个新类可以看作只是创建预定义类<class>的新实例, 例如


 (define <person>(make <class>
                        'direct-supers(list <object>)
                        'direct-slots(列表'名称'年龄)))

创建一个新类,将其“直接超类列表”初始化为单元素列表(<object>) ,将其“直接插槽列表”初始化为(name age) 。 但是,创建一个新类是一种常见的操作,它们提供了一个特殊的make-class函数来完成这项工作。 它需要两个参数:直接超类的列表(通常只有一个,直到你开始使用多重继承)和“槽”或实例变量的名称列表。 与所有Scheme变量一样,这些变量是无类型的:您同样可以插入数字38,符号bluebird ,列表(red green blue)或任何其他Scheme(或Tiny CLOS)对象。 因此,更常见的方法是创建上面的<person>类

(define <person> (make-class (list <object>) (list 'name 'age)))


实例变量

在OOP中,“实例变量”是与类的每个单独实例相关联的变量。 在上面的示例中, name和age是类<person>实例变量,因为每个人都有自己的(可能是不同的)名称和年龄。 实例变量可以被视为Pascal记录或C struct字段的更大,更好的版本。 CLOS字实例变量是“slot”。

要获取指定对象中指定插槽的值,请使用slot-ref函数,该函数接受两个参数 - 相关对象和插槽名称 - 并返回插槽的值:

(slot-ref sam'年龄) 
38

更改指定对象中指定插槽的值,请使用slot-set! 功能。 注意函数名末尾的感叹号,表明(与大多数Scheme函数不同)这个感知实际上改变了它的参数。 它需要三个参数:对象,插槽名称和新值。所以如果是Sam的生日,我们可能会写

(slot-set! sam 'age (+ 1 (slot-ref sam 'age)))

将插槽名称视为实现而不是接口通常被认为是良好的编程习惯,因此对象类的用户不会直接访问对象中的插槽。 这有两个原因:首先,如果用户开始依赖您的对象来包含具有特定名称的插槽,则您将失去通过重命名甚至消除其中一些实例变量来更改实现的自由; 第二,一个对象可能包含几个必须保持一致的相关信息,如果用户可以在你背后一次改变一条信息,这是一项基本上不可能完成的任务。 因此,大多数CLOS类具有“访问功能”,其目的仅仅是向用户提供关于该对象的某些信息,而用户不知道该信息是如何存储的(插槽名称,或者甚至是否存储在插槽中)在所有)。 另一种访问功能允许用户在不知道如何存储该信息的情况下改变关于对象的某些信息。 这让我们...

通用功能和方法

CLOS中通过称为泛型函数的函数提供多态性:具有(可能)若干不同定义( 方法 )的函数,其中一个函数在运行时根据参数的类进行选择。


创建通用函数

您可以使用make-generic函数在CLOS中创建一个新make-generic函数,该函数不带参数:

(define turn (make-generic))

您可能会使用函数add-method ,它会更频繁地为现有泛型添加方法。 实际上,如果将add-method应用于尚未使用make-generic定义make-generic ,它将为您定义它,因此您实际上根本不需要make-generic 。

(add-method turn this-method)


创建和附加方法

好的,所以你可以使用make-generic或add-method来创建泛型函数,并且(在后一种情况下)为它附加一个新的“方法”。 但什么是“方法”? 简而言之,方法是普通的Scheme函数定义,以及指示哪些类必须属于哪个类以便此方法适用的信息。 方法由名为make-method的函数构造,该函数接受两个参数,一个类列表和一个函数(通常表示为lambda -form)。 例如,


 (定义这个方法
         (make-method(list <dial> <number>)
                      (lambda(cnm表盘设定) 
                           (while(<(拨号位置)设置)
                                  (转向盘)))))

这里我们定义了一个方法,只要第一个参数是<dial> ,第二个是数字,它就适用。 它的主体是lambda -form,它带有两个名为dial和setting参数,并反复调高表盘直到其位置匹配或超过所需的设置。(我假设已经在某处写过turn-up position-of和开头。) 
你毫无疑问地注意到上面的lambda cnm的无法解释的“ cnm ”参数。 在大多数面向对象的语言中,方法可以(并且通常)与超类的相应方法做几乎相同的事情,但是在之前或之后需要一些额外的工作。 为了避免重写超类的方法中的所有代码,CLOS提供了一种方法来为同一个泛型调用超类的方法。 每当调用一个方法时,Tiny CLOS的实现就是给它一个名为“ call-next-method ”的函数作为它的第一个参数,这样如果它希望为同一个泛型调用超类的方法,它可以做所以只需调用call-next-method 。 或许不幸的是,即使您不打算使用call-next-method机制,每个方法都必须采用额外的第一个参数以防万一。 我们稍后将讨论如何使用此机制。

在实践中,您可能不会费心定义this-method ,然后将其添加到通用turn ; 相反,你可能会一步到位:


 (添加方法转 
         (make-method(list <dial> <number>)
                      (lambda(cnm表盘设定) 
                           (while(<(拨号位置)设置)
                                  (转向盘)))))

initialize通用

许多面向对象的系统都带有已经定义的各种类和方法,并期望用户创建子类并根据需要覆盖这些方法。 一个例子是initialize generic,只要make创建一个类的新实例,就会自动调用。 initialize的第一个参数是正在创建的对象,第二个参数是给出的任何额外参数的列表。

我知道这有点令人困惑; 这是一个可能有帮助的例子。 假设我们已经如上所述定义了<person>类,并且我们希望在创建<person>对象的同时提供一个人的姓名(以及可选的年龄)。 我们可能会写


 (add-method初始化
     (make-method(list <person>)
         (lambda(cnm obj initargs)
             (slot-set!obj'名称(car initargs))
             (除非(null(cdr initargs))
                     (slot-set!obj'age(cadr initargs))))))

现在我们可以通过输入创建一个名为“Sam”的人

(定义friend1(make <person>“Sam”))

和另一个25岁的人,通过打字命名为“杰夫”

(定义friend2(make <person>“Jeff”25))

由于initialize泛型的相当常见的用法只是初始化命名槽,我编写了一个<object>的子类,名为<init-object>其initialize方法接受一个槽名称和值的列表; 请参阅initargs.scm获取(相当简单的)源代码。 典型的用途是


 (define <person>(make-class(list <init-object>)(list'name'age))) (定义朋友(make <person>'name“Jane”'46岁))

这违反了用户不应该知道插槽名称的原则; <init-object>类只是方便快速构建说明性示例。

一个例子

让我们尝试一个更完整的例子,一个不需要假设功能来告诉机器人手臂转动表盘的例子。 手头的问题是跟踪一组学生,每个学生都在纽约一所假设的大学注册了各种课程。 问题描述中最明显的两个对象是“student”和“course”,所以让我们为它们创建类。 为了便于初始化,让我们将它们作为<init-object>子类。 事实上,由于<person>已经是<init-object>的子类,并且已经有了一个名字和一个年龄,让我们让<student>成为<person>的子类,只有其他信息和方法不是已经在<person>类中。


 (define <student>(make-class(list <person>)
                   (列出'学分'课程列表))) (define <course>(make-class(list <init-object>)
                   (列出'名称'房间'时间'教授'学生列表)))

由于我们不希望这些对象的用户知道插槽名称,因此我们将定义一些访问函数:


 (add-method get-name
      (make-method(list <student>)
            (lambda(cnm学生)(老虎机参考学生的名字)))) (add-method get-courses
      (make-method(list <student>)
            (lambda(cnm学生)(老虎机参考学生'课程列表)))) (add-method get-class
      (make-method(list <student>)
            (lambda(cnm学生)
                (让((学分(老虎机参考学生学分)))
                     (cond((<credits 30)'新生)
                           ((<学分60)'大二学生)
                           ((<credits 90)'junior)
                           (否则'大四)))))) (add-method get-name
      (make-method(list <course>)
            (lambda(cnm课程)(插槽参考课程'名称)))) (add-method get-room
      (make-method(list <course>)
            (lambda(cnm课程)(插槽参考课程'室)))) (add-method get-time
      (make-method(list <course>)
            (lambda(cnm课程)(插槽参考课程'时间)))) (add-method get-prof-name
      (make-method(list <course>)
            (lambda(cnm课程)(插槽参考课程'教授)))) (add-method get-roster
      (make-method(list <course>)
            (lambda(cnm课程)(插槽参考课程'学生列表))))

请注意,尽管大多数这些访问功能只是对slot-ref调用,但他们没有理由要求:如果用户可能想要学生的课程,但实施者决定存储学生完成的学分数,像get-class这样的“访问函数”可以完成自己的重要工作。

回想一下,我们创建了<init-object> <student>和<course>子类,以便于初始化。 但是,让我们坚持认为所有学生都没有课程,所有课程都是在没有学生的情况下创建的。 我们可以通过覆盖它们的initialize方法来实现:


 (add-method初始化
     (make-method(list <student>)
         (lambda(call-next-method student initargs)
             (呼叫下一方法)
             (slot-set!student'course-list())))) (add-method初始化
     (make-method(list <course>)
         (lambda(call-next-method course initargs)
             (呼叫下一方法)
             (slot-set!course'student-list()))))

换句话说,即使某些用户确实尝试提供课程列表或学生列表作为参数,他们也会在之后被设置回空列表。

当然,跟踪学生和课程的主要原因是前者采取后者。 所以我们想写两个函数add and drop ,每个函数都是学生和一个课程:( (add sam csc272)注册Sam for CSC 272,两者都将Sam添加到CSC 272的课程名单中,并将CSC 272添加到Sam的日程表中, while (drop sam csc272)将Sam从课程名单中删除,而CSC 272则从Sam的日程表中删除。

当然,事情可能会出错。 Sam可能会尝试添加他已注册的课程,或者删除他尚未注册的课程,或添加与他已经学习的其他课程相冲突的课程等。所以我们可能需要这些功能来返回一些如果它们不起作用的那种错误指示。 如果他们确实有效,让我们让他们返回#f ,这样可以很容易地测试结果, 例如 (if (add sam csc272) ...)


 (add-method add
     (make-method(list <student> <course>)
         (lambda(cnm学生课程)
             (cond((memv student(get-roster course))
                    “错误:学生已经在课程名单中”)
                   ((memv course(get-courses student))
                    “错误:课程已经在学生的课程列表中”)
                   (否则(加学生学生课程)
                         (加课程课程学生)
                         #F))))) (add-method drop
     (make-method(list <student> <course>)
         (lambda(cnm学生课程)
             (cond((不是(memv student(get-roster course)))
                    “错误:学生不在课程名单中”)
                   ((不是(memv course(get-courses student)))
                    “错误:课程不在学生的课程列表中”)
                   (否则(删除 - 学生学生课程)
                         (删除课程学生)
                         #F)))))

这些函数负责错误检查,但依赖于另外四个(尚未写入)函数add-student , add-course , remove-student和remove-course来进行实际更改。 由于这四个函数必须修改<student>和<course>对象的内部状态,因此我们将它们作为这些类的接口的一部分:

 (add-method add-student
     (make-method(list <student> <course>)
         (lambda(cnm学生课程)
             (老虎机!课程'学生列表
                 (缺点学生(获得名册课程)))))) (add-method add-course
     (make-method(list <course> <student>)
         (lambda(cnm课程学生)
             (老虎机!学生'课程列表
                 (综合课程(入门课程学生)))))) (add-method remove-student
     (make-method(list <student> <course>)
         (lambda(cnm学生课程)
             (老虎机!课程'学生列表
                 (delv学生(获得名册课程)))))) (add-method remove-course
     (make-method(list <course> <student>)
         (lambda(cnm课程学生)
             (老虎机!学生'课程列表
                 (delv课程(入门课程学生))))))


登录后方可回帖

登 录
信息栏

Scheme中文社区

推荐实现 ChezScheme / r6rs / r7rs large
theschemer.org
Q群: 724577239

精华导览

社区项目

包管理器:Raven
HTTP服务器:Igropyr (希腊火)
官方插件:vscode-chez

社区目标:

完善足以使Scheme工程化和商业化的库,特别是开发极致速度的Web服务器和ANN模块。

一直以来Scheme缺少一个活跃的中文社区,同时中文资料的稀少,导致大多数因为黑客与画家和SICP而接触Scheme的朋友,在学完SICP后无事可做,不能将Scheme转换为实际的生产力。最后渐渐的放弃。
同时Chicken等实现,却因效率问题无法与其他语言竞争。本社区只有一个目的,传播Scheme的文明之火,在最快的编译器实现上,集众人之力发展出足够与其他语言竞争的社区和库。


友情链接:

Clojure 中文论坛
函数式·China


Loading...