| ClipArtMag Science Blog |

Free Cliparts

Параметры для структурированных данных в Emacs Lisp

Перевод статьи - Options for Structured Data in Emacs Lisp

Автор - Chris Wellons

Источник оригинальной статьи:

http://nullprogram.com/blog/2018/02/14/

Так, ваш Emacs package вырос за пределами 12 или более строк кода, а данные, которыми он управляет, теперь структурированы и разнородный. Неофициальные простые старые списки, хлеб и масло любого lisp, больше не разрезают его. Вам действительно нужно чисто абстрагировать эту структуру, как для вашей собственной организации, так и для любого, кто читает ваш код.

С неофициальными списками в качестве структур вы можете регулярно задавать такие вопросы, как « “name” было сохранено в третьем элементе списка, или был в четвертом элементе?». Plist или alist помогает с этой проблемой, но они лучше подходят для неформальные, внешние данные, а не внутренние структуры с фиксированными слотами. Иногда кто-то предлагает использовать hash-таблицы в качестве структур, но hash-таблицы Emacs Lisp слишком тяжелы для этого. Hash-таблицы более подходят, когда сами ключи являются данными.

Определение структуры данных с нуля

Представьте себе пакет холодильника, который управляет собранием еды в холодильнике. Элемент еды может быть структурирован как простой старый список, с слотами в определенных положениях.

(defun fridge-item-create (name expiry weight)
  (list name expiry weight))

Функция, вычисляющая средний вес списка продуктов питания, может выглядеть следующим образом:

(defun fridge-mean-weight (items)
  (if (null items)
      0.0
    (let ((sum 0.0)
          (count 0))
      (dolist (item items (/ sum count))
        (setf count (1+ count)
              sum (+ sum (nth 2 item)))))))

Обратите внимание на использование (nth 2 item) в конце, используемое для получения веса элемента. Это волшебное число 2 легко испортить. Хуже того, если много кодов обращается к «weight» так, то будущие экстеншны будут заблокированы. Определение некоторых функций accessor решает эту проблему.

(defsubst fridge-item-name (item)
  (nth 0 item))

(defsubst fridge-item-expiry (item)
  (nth 1 item))

(defsubst fridge-item-weight (item)
  (nth 2 item))

defsubst определяет встроенную функцию, поэтому для этих аксессуаров фактически нет дополнительных затрат времени на выполнение по сравнению с голым nth. Так как они охватывают только getting слотов, мы также должны определить некоторый setter, используя встроенный пакет gv (обобщенная переменная).

(require 'gv)

(gv-define-setter fridge-item-name (value item)
  `(setf (nth 0 ,item) ,value))
(gv-define-setter fridge-item-expiry (value item)
  `(setf (nth 1 ,item) ,value))

(gv-define-setter fridge-item-weight (value item)
  `(setf (nth 2 ,item) ,value))

Это делает каждый слот установленным. Обобщенные переменные отлично подходят для упрощения API, поскольку в противном случае должно быть равное количество функций setter (fridge-item-set-name и т. д.). С обобщенными переменными оба находятся в одной точке входа:

(setf (fridge-item-name item) "Eggs")

Есть еще два значительных улучшения.

1. Что касается Emacs Lisp, это не настоящий тип. Тип этого - просто фикция, созданная соглашениями пакета. Было бы легко совершить ошибку при передаче произвольного списка этим функциям fridge-item, и ошибка не была бы поймана до тех пор, пока этот список имеет по крайней мере три элемента. Общим решением является добавление type tag: символ в начале структуры, которая идентифицирует его.

2. Это все еще связанный список, и nth должен пройти список (то есть O (n)) для извлечения элементов. Было бы гораздо эффективнее использовать вектор, превратив его в эффективную операцию O (1).

Обращение к обоим из них сразу:

(defun fridge-item-create (name expiry weight)
  (vector 'fridge-item name expiry weight))

(defsubst fridge-item-p (object)
  (and (vectorp object)
       (= (length object) 4)
       (eq 'fridge-item (aref object 0))))

(defsubst fridge-item-name (item)
  (unless (fridge-item-p item)
    (signal 'wrong-type-argument (list 'fridge-item item)))
  (aref item 1))

(defsubst fridge-item-name--set (item value)
  (unless (fridge-item-p item)
    (signal 'wrong-type-argument (list 'fridge-item item)))
  (setf (aref item 1) value))

(gv-define-setter fridge-item-name (value item)
  `(fridge-item-name--set ,item ,value))

;; And so on for expiry and weight...

До тех пор, пока fridge-mean-weight использует fridge-item-weight accessor, он продолжает работать без изменений во всех этих изменениях. Но, вот, это довольно много шаблонов для написания и поддержки каждой структуры данных в нашем пакете! Создание шаблонного кода является идеальным кандидатом для определения макроса. К счастью для нас, Emacs уже определяет макрос для генерации всего этого кода: cl-defstruct.

(require 'cl)

(cl-defstruct fridge-item
  name expiry weight)

В Emacs 25 и более ранних версиях, это невинно выглядящее определение расшифровывается, по существу, во всем вышеприведенном коде. Созданный код выражается в наиболее оптимальной форме для своей версии Emacs и использует многие из доступных оптимизаций, используя объявления функций, такие как side-effect-free и error-free. Он также настраивается, позволяя исключить тег типа (: named) - отбрасывание всех проверок типа - или использование списка, а не вектора в качестве базовой структуры (: type). Как грубая форма структурного наследования, он даже позволяет напрямую встраивать другие структуры (: include).

Две ловушки

Однако, есть пара ловушек. Во-первых, по историческим причинам макро будет определять две нераспространенные функции пространства имен: make-NAME и copy-NAME. Я всегда переопределяю их, предпочитая конструкцию -create для конструктора и бросание копира, так как это либо бесполезно, либо, что-то еще хуже, семантически неправильно.

(cl-defstruct (fridge-item (:constructor fridge-item-create)
                           (:copier nil))
  name expiry weight)

Если конструктор должен быть более сложным, чем просто устанавливать слоты, обычно определяется «private» конструктор (двойная тире в имени) и обертывает его конструктором «public», который имеет какое-то поведение.

(cl-defstruct (fridge-item (:constructor fridge-item--create)
                           (:copier nil))
  name expiry weight entry-time)

(cl-defun fridge-item-create (&rest args)
  (apply #'fridge-item--create :entry-time (float-time) args))

Другая ловушка связана с печатью. В Emacs 25 и более ранних версиях типы, определенные cl-defstruct, по-прежнему являются только типами. Они действительно просто векторы, насколько это касается Emacs Lisp. Одно из преимуществ этого заключается в том, что печать и чтение этих структур является «free», потому что векторы можно печатать. Тривиально сериализовать структуры cl-defstruct в файл. Именно так работает база данных Elfeed.

Ловушка заключается в том, что после того, как структура была сериализована, больше не меняется определение cl-defstruct. Теперь это определение формата файла, поэтому слоты заблокированы на месте. Навсегда.

Emacs 26 бросает ключ во все это, хотя он стоит того, в конечном счете. В Emacs 26 есть новый примитивный тип с собственным синтаксисом читателя: записи. Это похоже на hash-таблицы, которые становятся первоклассными в читателе в Emacs 23.2. В Emacs 26 cl-defstruct использует записи вместо векторов.

;; Emacs 25:
(fridge-item-create :name "Eggs" :weight 11.1)
;; => [cl-struct-fridge-item "Eggs" nil 11.1]

;; Emacs 26:
(fridge-item-create :name "Eggs" :weight 11.1)
;; => #s(fridge-item "Eggs" nil 11.1)

Пока слоты по-прежнему доступны с использованием isf, и все проверки типов все еще происходят в Emacs Lisp. Единственное практическое изменение - функция record используется вместо векторной функции при распределении структуры. Но в будущем это еще более увлекательно.

Основным краткосрочным недостатком является то, что это нарушает печатную совместимость через границу Emacs 25/26. Функция cl-old-struct-compat-mode может использоваться для некоторой степени обратной совместимости, но не вперед. Emacs 26 может читать и использовать некоторые структуры, напечатанные Emacs 25 и ранее, но обратное никогда не будет правдой. Эта проблема первоначально сработала встроенные пакеты Emacs, и когда Emacs 26 выпущен, мы увидим, что эти проблемы возникают во внешних пакетах.

Динамическая отправка

До Emacs 25 основной встроенный пакет для динамической диспетчеризации - функции, которые специализируются на типе времени выполнения своих аргументов, - это EIEIO, хотя он поддерживает только одну отправку (специализируется на одном аргументе). EIEIO принес большую часть общей системы объектов Lisp (CLOS) в Emacs Lisp, включая классы и методы.

Emacs 25 представил более сложный пакет динамической рассылки, называемый cl-generic. Он фокусируется только на динамической диспетчеризации и поддерживает множественную отправку, полностью заменяя динамическую часть отправки EIEIO. Поскольку cl-defstructdoes наследование и cl-generic выполняет динамическую диспетчеризацию, EIEIO действительно не так много - помимо плохих идей, таких как множественное наследование и комбинация методов.

Без любого из этих пакетов самым прямым способом создания отдельной отправки поверх cl-defstruct было бы переключение функции в один из слотов. Тогда «метод» - это просто оболочка, вызывающая эту функцию.

;; Base "class"

(cl-defstruct greeter
  greeting)

(defun greet (thing)
  (funcall (greeter-greeting thing) thing))

;; Cow "class"

(cl-defstruct (cow (:include greeter)
                   (:constructor cow--create)))

(defun cow-create ()
  (cow--create :greeting (lambda (_) "Moo!")))

;; Bird "class"

(cl-defstruct (bird (:include greeter)
                    (:constructor bird--create)))

(defun bird-create ()
  (bird--create :greeting (lambda (_) "Chirp!")))

;; Usage:

(greet (cow-create))
;; => "Moo!"

(greet (bird-create))
;; => "Chirp!"

Поскольку cl-generic знает типы, созданные cl-defstruct, функции могут специализироваться на них, как если бы они были родными типами. Гораздо проще позволить cl-generic выполнять всю тяжелую работу. Люди, читающие ваш код, тоже оценят это:

(require 'cl-generic)

(cl-defgeneric greet (greeter))

(cl-defstruct cow)

(cl-defmethod greet ((_ cow))
  "Moo!")

(cl-defstruct bird)

(cl-defmethod greet ((_ bird))
  "Chirp!")

(greet (make-cow))
;; => "Moo!"

(greet (make-bird))
;; => "Chirp!"

В большинстве случаев простая cl-defstruct будет отвечать вашим потребностям, имея в виду gotcha с именами конструкторов и копиров. Его использование должно ощущаться почти так же естественно, как определение функций.