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

时间:2016-01-15 15:40:16

标签: common-lisp clos

我想编写带有多个用户界面后端的代码(例如文本和图形),因此它们很容易切换。我的方法是使用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。

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

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

4 个答案:

答案 0 :(得分:5)

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

我可能(否则)使用你一直暗指的通用功能设计。

答案 1 :(得分:5)

Common Lisp Interface Manager和多个后端

支持多个后端的CLOS中的UI层示例是CLIM,Common Lisp Interface Manager。 您可以研究其软件设计。请参阅以下链接。例如,参见类似 port (与显示服务的连接), medium 等类的协议(绘图发生时,对应的协议类)到某种纸张的输出状态), sheet (用于绘画和输入的表面,大致类似于分层窗口), graft (表示为主机窗口),...在应用程序中打开一个端口(例如打开一个特定的窗口系统,如X11 / Motif),应用程序的其余部分应该基本保持不变。 CLIM的体系结构将其所有服务映射到特定的CLIM后端,后端为X11 / Motif(或您将使用的任何端口)提供接口。

例如,函数draw-line将绘制到 mediums 。然后,泛型函数 medium-draw-line*将为一个或多个 medium 子类实现各种版本的绘制线。

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

有点历史

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

<强>指针

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

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

Spec(包括Silica):Common Lisp Interface Manager 2.0 Specification

答案 2 :(得分:3)

为了补充其他答案,这个用例有两个库。两者都受到Magritte Meta Model的启发,你应该看看它。

一个是descriptions,可让您定义不同的观看次数&#39;一个对象。它没有使用CLOS而是Sheeple,这是一个基于原型的CL对象系统。早期的方法是MAO,它基于CLOS。它为标准插槽对象增加了3个额外的插槽。 attribute-label,attribute-function和attribute-value。属性函数a中的函数将slot-value转换为final表示,如果function为nil,则attribute-value中的值按原样使用。而label是值的描述,类似于html5表单中的标签。

答案 3 :(得分:1)

“后端”是指前端,对吗?如同用户交互的部分,而不是处理应用程序逻辑的部分?

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

编辑:当您想要在不同的算法之间切换时,最好的选择可能只是将接口定义为类,将所有不同的算法定义为子类。

(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))