在 Common Lisp 中定义多个后端的惯用方法?

Idiomatic way to define multiple backends in Common Lisp?

我想编写具有多个用户界面后端(例如文本和图形)的代码,以便它们易于切换。我的方法是使用 CLOS:

(defgeneric draw-user-interface (argument ui)
  (:documentation "Present the user interface")
  (:method (argument (ui (eql :tui)))
    (format t "Textual user interface! (~A)" argument))
  (:method (argument (ui (eql :gui)))
    (format t "Graphical user interface! (~A)" argument)))

这种方法乍一看似乎没问题,但它有一些缺点。为了简化调用,我定义了每个函数调用都会用到的参数ui-type,以简化后端的切换,但是在使用高阶函数时会出现问题:

(defparameter *ui-type* :tui
  "Preferred user interface type")

(draw-user-interface 3 *ui-type*)

;;; I can't use the following due to the `ui' argument:
;(mapcar #'draw-user-interface '(1 2 3))

;;; Instead I have to write this
(mapcar #'(lambda (arg)
            (draw-user-interface arg *ui-type*))
        '(1 2 3))

;; or this
(mapcar #'draw-user-interface
        '(1 2 3)
        (make-list 3 :initial-element *ui-type*))

;; The another approach would be defining a function
(defun draw-user-interface* (argument)
  (draw-user-interface argument *ui-type*))

;; and calling mapcar
(mapcar #'draw-user-interface* '(1 2 3))

如果采用这种方法,我们可以将通用函数命名为 %draw-user-interface,将包装函数命名为 draw-user-interface。

这是有效的方法还是有更直接的方法?问题是关于为相同的功能提供不同的后端,不一定是用户界面。

另一个用例可能是这样一种情况,当我有许多相同算法的实现(针对速度、内存消耗等进行了优化)并且我想以一种干净的方式切换它们时保留接口和参数类型.

"backend" 你是说前端吧?比如,用户与之交互的部分,而不是处理应用程序逻辑的部分?

最干净的选择是将您的程序分成一个库(它提供了程序的所有逻辑和功能,没有任何 UI 代码)和两个完全独立的 UI 程序不要自己实现任何功能,而只是使用库。如果需要,您当然可以有一个包装器来选择 运行 的接口。您应该将每个组件保留在自己的系统中。

编辑: 当你想在不同算法之间切换时,最好的选择可能是简单地将接口定义为 class,并将所有不同的算法定义为子classes。

(defclass backend () ())
(defgeneric do-something (backend x y))

(defclass fast-backend (backend) ())
(defmethod do-something ((backend fast-backend) x y)
  (format t "Using fast backend with arguments ~a, ~a.~%" x y))

(defclass low-mem-backend (backend) ())
(defmethod do-something ((backend low-mem-backend) x y)
  (format t "Using memory efficient backend with arguments ~a, ~a.~%" x y))

(defun main (x y)
  (let ((backends (list (make-instance 'fast-backend)
                        (make-instance 'low-mem-backend))))
    (dolist (b backends)
      (do-something b x y))))

另一个编辑: 如果您需要能够使用像 mapcar 这样的函数,您可能需要一个包含当前后端的全局变量。然后定义一个使用全局的包装函数。

(defparameter *backend* (make-instance 'fast-backend))
(defun foobar (x y)
  (do-something *backend* x y))

(defun main (x y)
  (foobar x y)
  (let ((*backend* (make-instance 'low-mem-backend)))
    (foobar x y))
  (foobar x y))

我会将后端实现为单独的 类,而不是四处传递关键字,因为那样可以让我将各种状态挂钩到一个对象中并保留它。

我可能(否则)使用您一直提到的通用功能设计。

Common Lisp 接口管理器和多个后端

CLOS 中支持多个后端的 UI 层的一个示例是 CLIM,Common Lisp 接口管理器。 你可以研究一下它的软件设计。见下面的链接。例如,参见 protocols around classes like port (a connection to a display service), medium(绘图发生的地方,协议 class 对应于某种 sheet 的输出状态),sheet(a用于绘制和输入的表面,大致类似于分层 windows),graft(sheet 代表主机 window),.. . 在一个应用程序中,打开一个端口(例如到一个特定的 window 系统,如 X11/Motif),应用程序的其余部分应该 运行 大部分不变。 CLIM 的架构将其所有服务映射到特定的 CLIM 后端,该后端提供 X11/Motif(或您将使用的任何端口)的接口。

例如,函数 draw-line 将绘制到 sheetsstreams媒介泛型函数 medium-draw-line* 将对一个或多个 medium subclasses 执行各种版本的绘图线。

总的来说,这不是很成功,因为可移植的用户界面层带来了复杂性,需要大量的开发和维护工作。在 20 世纪 90 年代中期,Lisp 应用程序的市场很小(参见 AI Winter),CLIM 不够好,实现是封闭源代码和/或专有的。后来开发了一个名为 McCLIM 的开源/免费实现,它创建了工作软件 - 但最终 developers/users 失去了兴趣。

一段历史

以前 Symbolics 开发了一个名为 'Dynamic Windows' 的用户界面系统。它于 1986 年发布。它在 Symbolics 操作系统中 运行 并且可以绘制到其本机 OS/Hardware 组合和 X11。从 1988 年左右开始,开发了一个便携 CLOS-based 版本。第一个可用版本(特别是 1991 年的 1.0 版)在几个平台上可用:Genera、X11、Mac 和 Windows。后来开发了一个新版本(2.0 版),它在各种系统上再次 运行,但包含一个复杂的 object-oriented 层,它提供了一个更明确的后端层,称为 Silica。这个后端层不仅支持便携式绘图之类的东西,还支持抽象 window 系统的一部分。更雄心勃勃的部分,比如对外观和感觉自适应的支持(滑块、window 样式、滚动条、菜单、对话框元素……)还没有完全解决,但至少作为第一代版本可用.

指针

A Guided Tour of CLIM, Common Lisp Interface Manager (PDF)

二氧化硅:Implementation Reflection in Silica (PDF)

规格(包括二氧化硅):Common Lisp Interface Manager 2.0 Specification

为了补充其他答案,这个用例有两个库。两者的灵感都来自Magritte Meta Model,你应该去看看。

一个是descriptions which allows you to define different 'views' of an object. It doesn't use CLOS but Sheeple, a prototype-based object system for CL. An earlier approach is MAO,它是基于CLOS的。它将 3 个额外的插槽添加到标准插槽对象。 attribute-label、attribute-function 和 attribute-value。 attribute-function a 中的函数将 slot-value 转换为最终表示,如果函数为 nil,则按原样使用 attribute-value 中的值。而label是对值的描述,类似于html5形式的labels。