About

虽然之前写的/之前的 MOP 阅读和学习尝试都有点中道崩猝. 要说这次可能有什么不同的话, 那就是至少我现在是带着项目进行的学习了. 嗯, 应该可以这么说吧… (cocamlx-cl).

注: 假如有兴趣的话, 不妨来试试 MOPDoc, 为 MOP 添加 documentation string. 这样就可以用 SLIME/SLY 来 inspect.

这里参考的是 AMOP (Art of MetaObject Protocol).

The Design and Implementation of Metaobject Protocols

How CLOS is Implemented

A Subset of CLOS

书中是用了一个 CLOS 的子集来做教学例子. 这里就用真实对应的 CLOS 和 closer-mop 进行联系.

以下的代码在 CLOSER-COMMON-LISP-USER 下运行.

注: 这部分建议读什么

感觉这部分属于是作者小小炫技了: 在 CLOS 上实现一个 CLOS 的子集. 但是我并不是很关心具体是怎么实现的, 所以读这一部分的时候基本是跳着读的.

其实这部分可以主要用来关注在 CLOS 的 interface 下面的具体发生了什么过程. 比如创建实例 make-instance 的时候, 发生了什么过程:

  • allocate-instance
  • initialize-instance

The Basic Backstage Structures

Representing Classes

standard-class 这个 Metaclass 类中, 有以下的 slots:

(mapcar #'slot-definition-name (class-slots (find-class 'standard-class)))
sb-pcl::%typesb-pcl::sourcesb-pcl::plistsb-pcl::namesb-pcl::class-eq-specializersb-pcl::direct-superclassessb-pcl::direct-subclassessb-pcl::direct-methodssb-pcl::%documentationsb-pcl::safe-psb-pcl::finalized-psb-pcl::%class-precedence-listsb-pcl::cpl-available-psb-pcl::can-precede-listsb-pcl::incompatible-superclass-listsb-pcl::wrappersb-pcl::prototypesb-pcl::direct-slotssb-pcl::slots

The defclass Macro

可以把 defclass 的过程看作是:

(defmacro defclass (name direct-superclasses direct-slots &rest options)
  `(ensure-class
    ',name
    :direct-superclasses ,(canonicalize-direct-superclasses
                           direct-superclasses)
    :direct-slots        ,(canonicalize-direct-slots
                           direct-slots)
    ,@(canonicalize-defclass-options options)))

其中 canonicalize- 的这些部分可以暂时当作是一个正则化的过程, 忽略.

注: 实际上可能并不是这样实现的 defclass.

Direct Superclasses

是一组 superclasses 的列表.

(defun canonicalize-direct-superclasses (direct-superclasses)
  (mapcar #'find-class direct-superclasses))

Direct Slots

  • name \(→\) :name name
  • initform \(→\) :initform initform :initfunction (lambda () initform)

    其中

    • :initfunction: 是一个无参数的 lambda 函数, 用于在 lexical 环境被调用生成初始化的值
    • :initform: 仅是被保留用于可读性
  • initarg \(→\) :initargs (initarg initarg...)
  • accessor \(→\) :reader accessor :writer (setf accessor)
  • reader \(→\) :readers (reader reader...)
  • writer \(→\) :writers (writer writer...)

例:

(ensure-class
 'class
 :direct-superclasses (...)
 :direct-slots  (list (list
                       :name            'name             ; slot-definition-name
                       :initform        'val              ; slot-definition-initform
                       :initfunction   #'(lambda () val)  ; slot-definition-initfunction
                       :initargs        '(:name)          ; slot-definition-initargs
                       :readers         '(name)           ; slot-definition-readers
                       :writers         '((setf name))))) ; slot-definition-writers

Class Options

被转换为 plist 作为更多的参数如下被 ensure-class 类似如下调用:

(make-instance 'standard-class ,@class-options-plist)

ensure-class

相当于是做了如下的事情:

  • make-instance: 从 Metaclass 创建了一个 class 类
  • (setf find-class): 在 class lookup table 中注册对应的类

Intializing Class Metaobjects

  • 生成 direct-superclasses
  • 向 superclass 的 direct-subclasses 添加自己
  • 定义 readeraccessor 函数
  • 以及处理其他的继承关系

Inheritance

  • 计算类的继承关系
  • 计算 slot: 从父类中继承, 并最终生成 effective-slot-definition

Printing Objects

调用 print-object.

Representing the Structure of Instances

  • Object identity
    • make-instance: 类似于如下的过程
      (let ((instance (allocate-instance class)))
        (apply #'initialize-instance instance initargs)
        instance)
      
    • initialize-instance, reinitialize-instance 都共同地使用了 shared-initialize
  • Slot storage:
    • (alocate-instance class &rest args): 为类实例及其 slots 划分空间
    • (slot-value instance slot-name): 获取实例的 slot 的值
    • (slot-boundp instance slot-name): 判断是否绑定了值
    • (slot-makunbound instance slot-name): unbind slot
    • (slot-exists-p instance slot-name): 判断是否存在
  • Classification:
    • class-of: 获得实例的类
  • Reclassification:
    • change-class
    • update-instance-for-different-class

Representing Generic Functions

The defgeneric Macro

相当于是把 defgeneric 变成 ensure-generic-function

类似于:

(defun ensure-generic-function (function-name &rest keys)
  (if (fboundp function-name)
      ...
      (let ((gf (apply #'make-instance 'standard-generic-function :name function-name keys)))
        (setf (fdefinition function-name) gf)
        gf)))

Representing Methods

同上, ensure-method.

Invoking Generic Functions

A Word About Preformance

Introspection and Analysis

这一部分读什么

这部分主要给了一堆的接口可以用来在拓展 CLOS 的时候编程地实现属性的读取和写入. 但是我没啥精力去抄书, 所以这部分不如直接看 API…

Extending the Language

Specialized Class Metaobjects

通过定义 Metaclass, 可以实现修改默认类实例的生成规则.

例:

(defclass counted-class (standard-class)
  ((counter :initform 0)))

(defmethod make-instance :after ((class counted-class) &key)
  (incf (slot-value class 'counter)))

一般的做法:

  1. 定义一个 metaobject class (defclass CLASS (standard-class) (...) ...)
  2. 在 CLOS 上修改对应的过程
  3. 创建对应 metaobject class 的实例 – 即受 metaclass 行为控制的类

Terminology

TerminologyExamples, Values
standard metaobject classesstandrad-class, standard-generic-function, standard-method
specialized metaobject classesuser defined subclasses of standard metaobject classes
standard methodsmethods
metaobjectsinstance of metaobject class
specialized class metaobjects,
metaobject class

Using Specialized Class Metaobject Classes

(defclass class (...)
  (...)
  (:metaclass metaclass)
  (...)
  (:metaclass-init-arg init-val))

Class Precedence Lists

使用 compute-class-precedence-list 来得到一个类的所有的依赖列表.

(compute-class-precedence-list (find-class 'coca:ns-window))
(#<coca.appkit:ns-window NSWindow 20BF3C908>
 #<coca.appkit::ns-responder NSResponder 20BF3D600>
 #<coca.foundation:ns-object NSObject 20BEDDD58>
 #<standard-class coca.objc:standard-objc-object>
 #<standard-class coca.objc::objc-pointer>
 #<standard-class common-lisp:standard-object>
 #<sb-pcl::slot-class sb-pcl::slot-object> #<sb-pcl:system-class common-lisp:t>)

Slot Inheritance

Slot Inheritance Protocol

  • (compute-slots class) 返回元素为 effective-slot-definition 的列表

    在调用 compute-slots 时, class-precedence-list 应当已经被初始化;

  • (compute-effective-slot-definition class slots) 用于合并 slots
    • slots: 元素为 direct-slot-definition 的列表

User Code for the Slot Extension

AMOP 里面用了一个 attributes-class 来作为例子. 这里直接抛开例子, 大概介绍一下过程:

  • 需要一个 metaclass
    • 通过 compute-effective-slot-definition 更改合并的逻辑或者参与合并的过程
    • 通过参与 compute-slots 过程来改变或者更新元数据

Other Inheritance Protocols

Precomputing Default Initialization Arguments

Slot Access

以下的接口可以重写默认的 slot 规则:

  • (slot-value-using-class class instance slot-name)
  • (slot-boundp-using-class class instance slot-name)
  • (slot-makunbound-using-class class instance slot-name)

Instance Allocation

以下的接口可以重写默认的 slot 初始化规则:

  • (allocate-instance class)
  • (slot-definition-allocation slot-definition)

Protocol Design

因为并不关心这一部分, 所以就略过了.

A Simple Generic Function Invocation Protocol

Generic Function Invocation

Functional and Procedural Protocols

Documenting Generic Functions vs Methods

Overriding the Standard Method

Layered Protocols

Improving Performance

Effective Method Functions

Method Functions

Discriminating Functions

Streamlined Generic Function Invocation

Protocol Design Summary

So, in Coca

About

在 coca 这个项目里面, 因为我希望能够实现一个比较优雅的 Class property 的读取. 于是希望能够实现这样的操作:

(slot-value objc-object 'objc-property)

来代替 (invoke objc-object objc-property):

核心的思路如下 (以下的内容建议配合代码 objc-class.lisp 进行阅读):

  • 通过注册一个 objc-property-slot 来作为 standard-slot-definition 功能的 mixin, 即对于 ObjC instance property, 其所有的逻辑性功能都在该类中实现
  • 在 MOP 中, 需要区分 direct-slot-definitioneffective-slot-definition, 前者类似于在 defclass 的时候定义的 slot, 在确认 Class 初始化的时候 (ensure-finalized), 会被转换/合并为 effective-slot-definition, 即实际所持有的 slot-definition

于是就可以实现一个在 coerce-to-objc-class 的时候, 自动化注册 slot-definition 的操作.

后记

AMOP 属于是那种, 从一个非常简单的小例子开始, 慢慢不知不觉就实现了一个庞大系统的书. 不过由于这个庞大系统的耦合性实在是太强了, 只实现一个简单的小组件/或者说, 只实现 defclass 好像没啥用 – 因为确实 OOP 的知识不是这本书应该讲的内容.

我现在想要的教科书应该也是类似这样的教科书, 从一些最简单的部分开始, 然后慢慢地构建一个庞大的系统 – 听起来好像和现在的教学没啥区别, 大家不都是从一个简单的, 几乎类似于公理一样的一些前提假设出发, 不断地添加新的知识点/记忆点吗? 好像确实, 但是这个构建的过程应当是可以复用的, 是可以重新定义的. 比如说实现了计算素数的函数, 那么之后就可以重复应用这个函数, 来实现更加复杂的功能, 然后在性能或者表达能力受限的时候, 重新返回过来去修正… 好像确实就应当是正常的教学设计本来应该的方式.

那么还应该添加一个可以预见的目标 – 应当是携带着目标来去实现最终的结果, 这样会有一个全局的视野 – 这样利用 REPL 编程的好处就可以显现出来了: 一开始可以构建一些不存在的辅助函数, 比如说先实现了一个类似于伪代码一样的主函数, 其中的各种功能分支像是占位符一样得先放在这里 – 也许是已经存在的, 也许是需要调优的, 也许是还没有实现的. 总之这样在之后的更细的小任务里面, 应该就不会迷失具体最终要做什么.