Идиоматический способ определить несколько бэкэндов в 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.

Это правильный подход или есть что-то более прямолинейное? Вопрос заключается в предоставлении разных бэкэндов для одной и той же функциональности, не обязательно пользовательского интерфейса.

Другим вариантом использования может быть ситуация, когда у меня есть много реализаций одного и того же алгоритма (оптимизированных по скорости, потреблению памяти и т. д.), и я хочу переключать их чистым способом, сохраняя интерфейс и типы аргументов.


person Daniel Kochmański    schedule 15.01.2016    source источник


Ответы (4)


Чтобы дополнить другие ответы, для этого варианта использования есть две библиотеки. Оба вдохновлены метамоделью Магритта, вы должны проверить это из.

Одним из них является descriptions, который позволяет вам определять различные "представления" объекта. Он использует не CLOS, а Sheeple, основанную на прототипе объектную систему для CL. Более ранний подход — MAO, основанный на CLOS. Он добавляет 3 дополнительных слота к стандартному объекту слота. метка-атрибута, функция-атрибута и значение-атрибута. Функция в функции-атрибуте a преобразует значение слота в окончательное представление, если функция равна нулю, значение в значении-атрибуте используется как есть. А метка — это описание значения, похожее на метки в html5-формах.

person PuercoPop    schedule 15.01.2016

Я бы реализовал бэкенды как отдельные классы, вместо того, чтобы передавать ключевое слово, так как это позволило бы мне привязывать различные состояния к объекту и сохранять их.

Я бы, вероятно, (иначе) использовал общий дизайн функций, на который вы ссылались.

person Vatine    schedule 15.01.2016
comment
Хороший аргумент относительно класса вместо символа (@jkiiski предложил то же самое). Однако это не решает проблему функции HO. Спасибо за мнение. Я отмечаю этот ответ как принятый. - person Daniel Kochmański; 15.01.2016
comment
@DanielKochmański Однако реальной проблемы с функцией HO нет. Функция, на которую вы смотрите, действительно имеет два аргумента (бэкэнд и вход). (mapcar (lambda (input) (do-stuff backend input)) inputs) — это стандартный способ сделать это. Вы также можете определить каррирующую функцию, чтобы вы могли делать: (mapcar (curry #'do-stuff backend) inputs), но, хотя это избавляет от необходимости печатать, это менее идиоматично (поскольку читатели должны выяснить, что делает ваш curry). - person Joshua Taylor; 16.01.2016

Common Lisp Interface Manager и несколько бэкендов

Примером уровня пользовательского интерфейса в CLOS, поддерживающего несколько бэкендов, является CLIM, Common Lisp Interface Manager. Вы можете изучить дизайн его программного обеспечения. См. ссылки ниже. См., например, протоколы вокруг таких классов, как port (подключение к сервису отображения), medium (где происходит рисование, класс протокола, соответствующий в состояние вывода для какого-либо листа), лист (поверхность для рисования и ввода, примерно похожая на иерархические окна), прививать (лист, который заменяет хост-окно), ... В приложении открывается порт (например, для определенной оконной системы, такой как X11/Motif), а остальная часть приложения должна работать в основном без изменений. Затем архитектура CLIM сопоставляет все свои службы с определенной серверной частью CLIM, которая предоставляет интерфейс для X11/Motif (или любого другого порта, который вы будете использовать).

Например, функция draw-line будет рисовать на листах, потоках и носителях. Затем общая функция medium-draw-line* реализует различные версии рисования линий для одного или нескольких подклассов средних.

В целом это было не очень успешным, потому что уровень переносимого пользовательского интерфейса усложняет и требует много работы для разработки и поддержки. В середине 90-х рынок приложений Lisp был небольшим (см. AI Winter), CLIM был недостаточно хорош, а реализация была закрытой или проприетарной. Позже была разработана бесплатная реализация с открытым исходным кодом под названием McCLIM, которая создала работающее программное обеспечение, но в конечном итоге разработчики/пользователи потеряли интерес.

Немного истории

В прежние времена Symbolics разработала систему пользовательского интерфейса под названием «Динамические окна». Он был выпущен в 1986 году. Он работал в операционной системе Symbolics и мог использовать собственную комбинацию ОС / аппаратного обеспечения и X11. Примерно с 1988 года была разработана портативная версия на основе CLOS. Первые доступные версии (особенно версия 1.0 в 1991 году) были доступны на нескольких платформах: Genera, X11, Mac и Windows. Позже была разработана новая версия (версия 2.0), которая снова работала на различных системах, но включала сложный объектно-ориентированный слой, который предоставлял более явный внутренний слой под названием Silica. Этот внутренний слой поддерживал не только такие вещи, как портативное рисование, но и части абстрактной оконной системы. Более амбициозные части, такие как поддержка адаптации внешнего вида (ползунки, стили окон, полосы прокрутки, меню, элементы диалога и т. д.), не были полностью проработаны, но, по крайней мере, были доступны в версии первого поколения.

Указатели

Экскурсия по CLIM, диспетчеру интерфейса Common Lisp (PDF)

Кремнезем: Реализация отражения в кремнеземе (PDF)

Спецификация (включая Silica): Common Lisp Interface Manager 2.0 Specification

person Rainer Joswig    schedule 15.01.2016

Под «бэкендом» ты подразумеваешь фронтенд, верно? Например, часть, с которой взаимодействует пользователь, а не часть, которая обрабатывает логику приложения?

Самым чистым вариантом было бы разделить вашу программу на библиотеку (которая предоставляет всю логику и функции программы без какого-либо кода пользовательского интерфейса) и две совершенно отдельные программы пользовательского интерфейса, которые сами не реализуют никаких функций, а просто используют библиотеку. Конечно, вы можете иметь оболочку, которая выбирает, какой интерфейс запускать, если это необходимо. Вы должны держать каждый компонент в своей собственной системе.

Редактировать: если вы хотите переключаться между различными алгоритмами, лучшим вариантом, вероятно, будет просто определить интерфейс как класс, а все различные алгоритмы — как подклассы.

(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, вы можете захотеть иметь глобальную переменную, содержащую текущий бэкенд. Затем определите функцию-оболочку, которая использует метод global.

(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))
person jkiiski    schedule 15.01.2016
comment
Под бэкендом я подразумеваю бэкенд — у нас может быть три реализации одного и того же алгоритма (назовем его XXX123), каждая со своими характеристиками (одна оптимизирована по скорости, другая — по потреблению памяти, а третья — некий компромисс между ними обоими) . Я хочу переключать реализации чистым способом, сохраняя тот же интерфейс и типы аргументов. - person Daniel Kochmański; 15.01.2016
comment
Ваш пост в этом отношении весьма сбивает с толку. Возможно, вам следует оставить все, что связано с рисованием пользовательских интерфейсов, и просто сосредоточиться на различных алгоритмах. Наличие нескольких пользовательских интерфейсов — совсем другая проблема. - person jkiiski; 15.01.2016
comment
Что представляет собой бэкэнд или внешний интерфейс, зависит только от вашей точки зрения. Для программы, рисующей графику, логический бэкенд (то, с чем вы что-то делаете) — это то, что отображает пользовательский интерфейс для пользователя. - person Vatine; 15.01.2016
comment
Пользовательский интерфейс — это всего лишь пример, и я считаю, что подробно объяснил, чего я хочу достичь и какое решение пришло мне в голову. Я также прямо сказал, что это более общий вопрос. Заметив, что вы запутались, я добавил еще один пример, отличный от пользовательского интерфейса (который, я считаю, тоже очень действителен). - person Daniel Kochmański; 15.01.2016
comment
Что касается ответа, ваше решение похоже на первый подход (без функции-оболочки), но вместо символа вы предоставляете некоторый класс (что дает мне возможность передать какое-то состояние, как указал @Vatine на второй ответ). Обратите внимание, что мне пришлось бы изменить каждый (сделать что-то b x y) аргумент b, чтобы изменить поведение (или использовать некоторый глобальный объект my-b). В любом случае у меня будут те же проблемы с функциями высшего порядка, которые я перечислил (что не имеет большого значения, мне просто интересно, есть ли общепринятый подход к таким вещам). - person Daniel Kochmański; 15.01.2016
comment
@Vatine Я не согласен с этим. В приложении, рисующем графику, бэкенд по-прежнему будет библиотекой, обрабатывающей рисование (предоставляя такие функции, как (draw-rectangle surface x y w h)). Внешний интерфейс может быть графическим интерфейсом, который считывает ввод с мыши, или интерфейсом командной строки для использования скриптами. Очень маловероятно, что эти два внешних интерфейса будут иметь общий код, и поэтому их следует реализовывать в отдельных системах с использованием общей внутренней библиотеки. - person jkiiski; 15.01.2016
comment
@jkiiski А то, к чему в X11 подключены экран, клавиатура и мышь, — это сервер или клиент? Что такое бэкенд и что такое фронтенд, зависит исключительно от того, в каком направлении вы смотрите. Для разработчиков X11 и протоколов ответом является сервер (программа, которая хочет рисовать на экране или читать с клавиатуры, является клиентом). - person Vatine; 15.01.2016
comment
@jkiiski относительно второго редактирования - это функция-оболочка, которую я предложил в вопросе, если он идиоматичен. - person Daniel Kochmański; 15.01.2016
comment
@Vatine Когда у вас есть две программы, взаимодействующие друг с другом, у них, вероятно, есть бэкэнд и внешний интерфейс, так что в этом смысле вы правы. Однако, когда у вас есть пользователь, напрямую взаимодействующий с программой, я бы сказал, что бэкенд — это часть, которая реализует функции, а внешний интерфейс — это часть, которая переводит ввод пользователя в вызовы функций и возвращает значения в пиксели на экране. Даниэль Когмански: Верно. Я добавил его, чтобы показать, что ИМО это лучшее решение. - person jkiiski; 15.01.2016
comment
В переносимой UIMS внешний интерфейс — это код, который предоставляет переносимый API и услуги. Например, абстрактные средства рисования линий с заданными параметрами (цвет, толщина, стили линий и т. д.). Будет независимый от оконной системы способ указать их. Бэкенд — это конкретное сопоставление с реальной оконной системой. Это будет взаимодействовать со специальным кодом GUI (GTK+, Cocoa, ...), который в конечном итоге рисует линии в пикселях на экране. Здесь используются специфические для окна параметры. - person Rainer Joswig; 16.01.2016