【问题标题】:Idiomatic way to define multiple backends in Common Lisp?在 Common Lisp 中定义多个后端的惯用方式?
【发布时间】:2016-01-15 15:40:15
【问题描述】:

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

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

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

【问题讨论】:

    标签: common-lisp clos


    【解决方案1】:

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

    我可能(否则)会使用您提到的通用函数设计。

    【讨论】:

    • 关于类而不是符号的好论据(@jkiiski 提出了相同的建议)。但它并没有解决 HO 函数问题。感谢您的意见。我将此答案标记为“已接受”。
    • @DanielKochmański 不过,没有真正的 HO 函数问题。您正在查看的函数实际上有两个参数(后端和输入)。 (mapcar (lambda (input) (do-stuff backend input)) inputs) 是执行此操作的标准方法。你也可以定义一个柯里化函数,这样你就可以这样做:(mapcar (curry #'do-stuff backend) inputs),但是虽然这样可以节省一些打字,但它不那么惯用(因为读者必须找出你的 curry 做了什么)。
    【解决方案2】:

    Common Lisp Interface Manager 和多个后端

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

    例如,函数 draw-line 将绘制到 sheetsstreamsmediums通用函数 medium-draw-line* 会为一个或多个medium 子类实现不同版本的画线。

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

    一点历史

    在以前,Symbolics 开发了一种称为“动态窗口”的用户界面系统。它于 1986 年发布。它在 Symbolics 操作系统中运行,可以利用其原生操作系统/硬件组合和 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)

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

    【讨论】:

      【解决方案3】:

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

      一个是descriptions,它允许您定义对象的不同“视图”。它不使用 CLOS,而是使用Sheeple,这是一个基于原型的 CL 对象系统。较早的方法是MAO,它是基于 CLOS 的。它向标准插槽对象添加了 3 个附加插槽。属性标签、属性功能和属性值。属性函数 a 中的函数将槽值转换为最终表示,如果函数为 nil,则按原样使用属性值中的值。而label是对值的描述,类似于html5表单中的labels。

      【讨论】:

        【解决方案4】:

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

        最简洁的选择是将您的程序分成一个库(它提供程序的所有逻辑和功能,而无需任何 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))
        

        【讨论】:

        • “后端”是指“后端”——我们可能有相同算法的三种实现(我们称之为 XXX123),每种都有自己的特点(一种针对速度进行优化,另一种针对内存进行优化消费,第三个是两者之间的某种妥协)。我想以干净的方式切换实现,保留相同的接口和参数类型。
        • 你的帖子在这方面相当混乱。也许你应该省略所有关于绘制用户界面的内容,而只关注不同的算法。拥有多个 UI 是完全不同的问题。
        • 什么是“后端”或“前端”完全取决于你有什么观点。对于一个绘制图形的程序,逻辑后端(你要做的事情)是向用户显示 UI 的东西。
        • UI 只是一个例子,我相信我已经详细解释了我想要实现的目标以及我想到的解决方案。我也明确说过,这是更笼统的问题。在注意到您感到困惑之后,我添加了另一个与 UI 不同的示例(我相信这也非常有效)。
        • 关于答案,您的解决方案类似于第一种方法(没有包装器功能),但您提供的不是一个符号类(什么让我能够传递一些状态,正如@Vatine 指出的那样关于第二个答案)。请注意,我必须更改每个 (do-something b x y) 参数 b,以更改行为(或使用一些全局对象 my-b)。无论哪种方式,我都会遇到与我列出的高阶函数相同的问题(没什么大不了的,我只是想知道这些事情是否有普遍认可的方法)。
        猜你喜欢
        • 2015-05-10
        • 2011-12-24
        • 2012-07-12
        • 1970-01-01
        • 2019-05-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多