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

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

Таким чином, ваш пакет Emacs виростав за межами дюжини рядків коду, а дані, які він керує, тепер структуровані та неоднорідні. Неформальні прості старі списки, хліб та вершкове масло будь-якого листу більше не розрізають його. Ви дійсно повинні чисто абстрагувати цю структуру, як для власної організації, так і для когось, хто читає ваш код.

З неофіційними списками, як структурами, ви можете регулярно ставити питання типу: "Чи був" слот імені "збережений у елементі третього списку, чи був він четвертим елементом?" Пліст або аліст допомагає з цією проблемою, але вони краще підходять для неформальні дані, що постачаються ззовні, а не для внутрішніх структур з фіксованими слотами. Інколи хтось пропонує використовувати хеш-таблиці як структури, але хеш-таблиці Emacs Lisp набагато важливі для цього. Хеш-таблиці є більш доцільними, коли самі ключі є даними.

Визначення структури даних з нуля

Уявіть собі пакет холодильника, який управляє збором їжі в холодильнику. Продукт харчування може бути структурований як звичайний старий список із слотами на певних позиціях.

(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 легко збити. Ще гірше, якщо багато кодів доступу "вага" таким чином, то майбутні розширення будуть заважати. Визначення деяких функцій доступу вирішує цю проблему.

(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 . Оскільки вони охоплюють тільки слоти, ми також повинні визначити деякі встановлювачі, використовуючи вбудований пакет 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, тому що інакше потрібно мати рівну кількість функцій fridge-item-set-name ( fridge-item-set-name та ін.). З узагальненими змінними обидва розташовані в одній і тій же точці входу:

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

Є ще ще два значні поліпшення.

  1. Що стосується Emacs Lisp, це не є реальним типом . Тип його - це просто фантастика, створена конвенціями пакета. Було б легко зробити помилку при передачі довільного списку до цих функцій fridge-item , і помилка не буде зловитись до тих пір, поки в цьому списку буде принаймні три пункти. Загальним рішенням є додавання тегу типу : символ на початку структури, який його ідентифікує.

  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 використовує аксесуар для fridge-item-weight , вона продовжує працювати без змін у всіх цих змінах. Але, привело , це дуже багато чіпси для написання та підтримки для кожної структури даних у нашому пакеті! Початковий код кодування є ідеальним кандидатом для визначення макросу. На щастя, для нас, 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)

Якщо конструктор повинен бути більш витонченим, ніж просто встановити слоти, загальноприйнято визначити "приватний" конструктор (подвійний тире в імені) і обернути його "публічним" конструктором, який має певну поведінку.

(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. Одним з переваг цього полягає в тому, що друк і читання цих структур є "вільними", оскільки вектори можна роздрукувати. Це тривіальне, щоб серіалізувати структури cl-defstruct до файлу. Саме так працює база даних Elfeed .

Крихіткою є те, що як тільки структура була серіалізована, більше не змінюється визначення cl-defstruct . Тепер це визначення формату файлу, тому слоти заблоковано на місці. Назавжди

Emacs 26 кидає ключ у всьому цьому, хоча це коштує в довгостроковій перспективі. У Emacs 26 новий тип примітиву з власним синтаксисом читачів: записи. Це схоже на хеш-таблиці, які стають першим класом читача в 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)

Поки слоти все ще доступні, використовуючи aref , і все перевірка типу все ще відбувається в Emacs Lisp. Єдиною практичною зміною є функція record яка використовується замість vector функції при розподілі структури. Але це прокладає шлях для більш цікавих речей у майбутньому.

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

Динамічна доставка

До Emacs 25 найбільший вбудований пакет для динамічної диспетчеризації - функції, які спеціалізуються на стилі виконання своїх аргументів - був EIEIO, хоча він підтримував лише одиничну доставку (спеціалізуючись на одному аргументі). EIEIO приніс більшу частину Common Lisp Object System (CLOS) до Emacs Lisp, включаючи класи та методи.

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

Без будь-якого з цих пакетів, найбільш прямий спосіб побудувати окрему розсилку на вершині cl-defstruct полягає в тому, щоб 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 буде виконувати ваші потреби, маючи на увазі готча за допомогою імен конструктора та копіра. Її використання має відчувати себе настільки ж природним, що і визначення функцій.