首页 文章

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

提问于
浏览
3

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

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

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

4 回答

  • 5

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

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

  • 3

    Common Lisp Interface Manager and multiple backends

    支持多个后端的CLOS中的UI层的示例是CLIM,Common Lisp Interface Manager . You could study its software design. 请参阅以下链接 . 例如,参见类似端口的协议(与显示服务的连接),介质(绘图发生的地方,对应于某种工作表的输出状态的协议类),工作表(用于绘制和输入的表面,大致类似于分层窗口),移植(一个代表主机窗口的工作表),...在一个应用程序中打开一个端口(例如到特定的窗口系统,如X11 / Motif),应用程序的其余部分应该是运行基本不变 . CLIM的体系结构将其所有服务映射到特定的CLIM后端,后端为X11 / Motif(或您将使用的任何端口)提供接口 .

    例如,函数 draw-line 将绘制到工作表,流和媒体 . 然后,通用函数 medium-draw-line* 将实现各种版本的绘制线到一个或多个中等子类 .

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

    A bit of history

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

    Pointers

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

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

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

  • 5

    为了补充其他答案,这个用例有两个库 . 两者都受到启发Magritte Meta Model,你应该检查一下 .

    一个是descriptions,它允许您定义一个对象的不同'views' . 它不使用CLOS而是Sheeple,这是一个基于原型的CL对象系统 . 早期的方法是MAO,这是基于CLOS的 . 它为标准插槽对象添加了3个额外的插槽 . attribute-label,attribute-function和attribute-value . 属性函数a中的函数将slot-value转换为最终表示,如果function为nil,则attribute-value中的值按原样使用 . 而label是值的描述,类似于html5表单中的标签 .

  • 1

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

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

    Edit: 当你想在不同的算法之间切换时,最好的选择可能只是将接口定义为一个类,将所有不同的算法定义为子类 .

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

    Another edit: 如果您需要能够使用 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))
    

相关问题