В предыдущих разделах мы описали, как создавать сложные спецификации типов для defcustom. В некоторых случаях может потребоваться дать такой спецификации типа имя. Очевидный случай — когда вы используете один и тот же тип для многих пользовательских опций: вместо повторения спецификации для каждой опции вы можете дать спецификации типа имя и использовать это имя в каждом defcustom. Другой случай — когда значение пользовательской опции представляет собой рекурсивную структуру данных. Чтобы тип данных мог ссылаться сам на себя, ему нужно иметь имя.
Поскольку пользовательские типы реализованы как виджеты, способ определения нового типа настройки — это определение нового виджета. Мы не будем подробно описывать здесь интерфейс виджетов, для этого см. Введение в Библиотеке виджетов Emacs. Вместо этого мы продемонстрируем минимальную функциональность, необходимую для определения новых типов настройки, на простом примере.
(define-widget 'binary-tree-of-string 'lazy
"Двоичное дерево, состоящее из cons-ячеек и строк."
:offset 4
:tag "Узел"
:type '(choice (string :tag "Лист" :value "")
(cons :tag "Внутренний"
:value ("" . "")
binary-tree-of-string
binary-tree-of-string)))
(defcustom foo-bar ""
"Пример переменной, хранящей двоичное дерево строк."
:type 'binary-tree-of-string)
Функция для определения нового виджета называется define-widget. Первый аргумент — это символ, который мы хотим сделать новым типом виджета. Второй аргумент — символ, представляющий существующий виджет; новый виджет будет определяться через отличия от существующего виджета. Для целей определения новых типов настройки виджет lazy идеален, поскольку он принимает ключевой аргумент :type с тем же синтаксисом, что и одноимённый ключевой аргумент defcustom. Третий аргумент — строка документации для нового виджета. Вы сможете увидеть эту строку с помощью команды M-x widget-browse RET binary-tree-of-string RET.
После этих обязательных аргументов следуют ключевые аргументы. Самый важный из них — :type, который описывает тип данных, которые мы хотим сопоставлять с этим виджетом. Здесь binary-tree-of-string описывается как либо строка, либо cons-ячейка, чьи car и cdr сами являются binary-tree-of-string. Обратите внимание на ссылку на тип виджета, который мы сейчас определяем. :tag — ещё один важный ключевой аргумент, поскольку мы используем виджет lazy для нашего нового виджета. По умолчанию виджет lazy не имеет метки, и в её отсутствие буфер настройки будет показывать всё значение виджета (то есть значение настраиваемой пользовательской опции). Поскольку это почти никогда не является хорошей идеей, мы предоставляем строку для именования виджета binary-tree-or-string. Аргумент :offset нужен для того, чтобы дочерние узлы были смещены на четыре пробела относительно родительского узла, делая структуру дерева очевидной в буфере настройки.
Пример defcustom показывает, как новый виджет можно использовать в качестве обычного типа настройки.
Причина названия lazy (ленивый) в том, что другие составные виджеты преобразуют свои подчинённые виджеты во внутреннюю форму при инстанцировании виджета в буфере. Это преобразование рекурсивно, поэтому подчинённые виджеты будут преобразовывать свои подчинённые виджеты. Если структура данных сама рекурсивна, это преобразование представляет собой бесконечную рекурсию. Виджет lazy предотвращает рекурсию: он преобразует свой аргумент :type только тогда, когда это необходимо.