Elisp.
Lisp — это не язык, а строительный материал. (Алан Кэй)
Elisp - расширеяемый функциональный язык для emacs
Все что я опишу ниже - плод моего изучения. Рекомендую изучать язык по ссылкам приведенным ниже. Я могу ошибаться, а также неправильно интерпритировать изученный мной материал. Также, может показаться что я отношусь к лиспу как к неочень хорошо спроектированному языку. Это не так. Я отношусь так ко всем языкам. При этом автор понятия не имеет как можно что-то улучшить, и вообще… не стоит тратить время на его писульки.
Ссылки
Ресурсы для ознакомления
Quick Start.
Быстрый старт для тех кто уже умеет программировать.
Типы данных, переменные, константы
Основа языка
Объевление переменной
Такие переменные объявляются в глобальном скоупе (либо переопределяется в локальном. Локальный скоуп - let
, но об этом ниже). Т.к. в лиспе нет изоляции на уровне модуля то хорошей практикой является использование префиксов. Часто префиксы сопостовимы с название пакета. Например ivy–sort.
(setq var "my-package--variable")
Defcustom
Переменные которые могут настраиваться с помощью customize - специального меню с ui полями ввода. Значение для переменной defcustom
можно выбирать из списка: :options
. Разработчик плагина может заранее задать список возможных значений для таких перменных. :group
- значение которое позволяет группировать несколько переменных в группу, для более удобного редактирования. Как я понял defcustom
почти всегда > setq
.
(defcustom my-custom-variable "hello"
"Some description"
:type 'string
:group 'my-custom-group)
(message my-custom-variable)
Объявление локальной переменой
При любой возможности стоит выбирать локальную переменную, т.к. это изолирует функционал и сводит случайную перезапись к минимуму.
(let ((my-var "I'am a local variable"))
(message my-var))
Существует ограничение, такую переменную нельзя периспользовать в блоке let
. Чтобы ее можно было переиспользовать используется let*
. Лично я используею везде let*
.
(let* ((my-var "I'am a local variable")
(my-var (concat my-var " And i can be overwrited!")))
(message my-var))
Локальную переменную можно перезаписать, иногда это поволяет сократить избыточный код. setq
в данном случае перезапишет локальный скоуп.
(let* ((name "Oleg"))
(message name)
(setq name "Vasya")
(message name))
Работа с char
Char в лиспе обозначается знаком вопроса. Конвертация осуществляется с помощью функции string
, либо если это список из символов то с помощью функции concat
(let ((my-awesome-char ?Q))
(message (string my-awesome-char ?H ?e ?e ?l ?o))
(message (concat '(?W ?o ?r ?l ?d))))
С помощью символов можно сделать repeat
(make-string 10 ?|)
Работа со строками
Форматирование строки
(message (format "Hello %s\n" "World?"))
Списки
Списки “экранируются” (на самом деле это не экранирование, т.к. все в лиспе функция это просто указатель на то что это не нужно исполнять, называется это evaluate но это конечно же не точно) с помощью симола ’
(setq my-first-list '("Foo" "Baz" "Qwe"))
Получить первый элемент
(car my-first-list)
Получить все кроме первого элемента..
(cdr my-first-list)
Добавить элемент в список
Push мутирует список. Не выглядит как нечто функциональное, но возможно я что-то не понял.
(setq my-first-list '()) (push "Lalalend" my-first-list) (push "Hey" my-first-list)
Ну или так (последний аргумент t - добавить в конец)
(setq my-test-2-list '("qweqweqwe" "123")) (add-to-list 'my-test-2-list "qwe" t) (message "%s" my-test-2-list)
Слияние 2 списков
(setq my-first-list '(?q ?b ?c)) (setq my-first-list (append my-first-list (list ?t))) (message "%s" my-first-list)
Map
На самом деле
mapcar
(возможно создатель языка хотел иметь машину…).(defun greeting (name) (format "Hello %s" name)) (mapcar 'greeting my-first-list)
forEach
mpcar
создает новый список, можно просто итерироваться по записям с помощь. dolist(let* ((v "")) (dolist (p '("one" "two" "three")) (setq v (concat v " " p))) (message v))
Проверить есть ли элемент в списке
(member "123" '(1233 "qwe" "123"))
Перезаписать элемент в списке по индексу
(setq my-test-list '(("qwe" . 1) ("be" . 2))) (setcdr (assoc "qwe" my-test-list) "asdlkajsdakd") (message "%s" my-test-list)
Ассоциативные массивы
Объявление
(setq trees '((a . 1) (b . "qwe")))
При чем точка нужна для специального типа symbols. Если работает с реальными значениями то можно и без нее
(setq another-hashmap '(("a" "First elem") ("b" "Second elem")))
Получить элемент по ключу
(message "%s" (assoc 'a trees))
Ну и конечно возвращает оно кортеж..а чтобы получить элемент нужно использовать уже известную нам функцию -
cdr
(message "%s" (cdr (assoc 'a trees)))
Получить элемент по значению
(message "%s" (rassoc "qwe" trees))
При этом rassoc работает и для строк и для чисел, а вот rassq только для чисел
(message "%s" (rassq "qwe" trees)) ;; nil (message "%s" (rassq 1 trees)) ;; (a . 1)
Копирование мапы
(setq needles-per-cluster '((2 . ("Austrian Pine" "Red Pine")) (3 . ("Pitch Pine")) (5 . ("White Pine")))) (setq copy (copy-alist needles-per-cluster)) (message "%s" copy)
Удаление всех записей по ключу
(setq alist (list '(foo 1) '(bar 2) '(foo 3) '(lose 4))) (setq new-alist (assq-delete-all 'foo alist)) ;; Возвращает новое значение (message "%s" new-alist) (message (concat (format "alist: %s\n" alist) (format "new: %s" new-alist)))
Удаление записей по значению
(setq alist2 '((foo . first) (bar . second) (foo2 . third) (qwe . five))) (setq new-alist (rassq-delete-all 'third alist2)) ;; меняет значение ? (message "%s" new-alist) (message (concat (format "alist: %s\n" alist2) (format "new: %s" new-alist))) ;; (message "%s" (rassq 'foo alist2))
Хешмап
(setq my-first-map #s(
hash-table
size 10
test equal
data (
python-mode "spam!"
go-mode "booo!1 terrible pointer"
org-mode "amma fluffy feature ;p"
)))
(puthash 'js-mode "ugly language" my-first-map)
(message "%s" (gethash 'python-mode my-first-map))
(message "%s" (gethash 'js-mode my-first-map))
Символ
Тип данные соотвутствующий объекту с именем. Задаются символы с помощью 1 начальной кавычки. 'amma-symbol
Функции
Читать про функции
Объявление функций
Функции принято комментировать, это позволяет смотреть документацию в автодополнении. Вызов (interactive)
означается что функция публичная и может быть взывана пользователем напрямую, либо через сочетание клавиш.
(defun hello (my-name)
"This function will say hello for MY-NAME."
(interactive)
(message (concat "Hello, I'am " my-name)))
(hello "Artur")
Опицональные аргументы
(defun my-super-optional-function (name &optional last-name patronymic)
(message "%s %s %s" name (or last-name "") (or patronymic "")))
(my-super-optional-function "Artur" nil "Proshkov")
Именованные аргументы
(defun my-super-function-with-named-args (&rest args)
(message "Name %s, middle name %s" (plist-get args :name) (plist-get args :middle-name)))
(my-super-function-with-named-args :name "One" :middle-name "Dude")
Лямбды
Очевидно, лямбды нужны чтобы код можно было хуже читать
(funcall '(lambda () (message "I'am dirty func")))
Advice
Адвайсы это прокаченные декораторы. Могут быть вызваны как до так и после вызова оригинальной функции.
(defun my-increment (n)
(+ n 1))
(defun mux-5 (n)
(* n 5))
(advice-add 'my-increment :filter-return #'mux-5)
(message "%s" (my-increment 10))
Пример адвайса после выполненеия функции
(defun my-first-func()
(message "qweqwe"))
(my-first-func)
(defun my-adv()
(message "advice called"))
(advice-add :after 'my-first-func #'my-adv)
(my-first-func)
Property list (plist)
Установка и запись
(setq my-plist '(:is-enabled t :another-prop "hey"))
(message "enabled: %s, another prop: %s" (plist-get my-plist :is-enabled) (plist-get my-plist :another-prop))
Изменение
(setq my-plist '(:is-enabled t :another-prop "hey"))
(plist-put my-plist :another-prop "Wow, i was changed")
(message "enabled: %s, another prop: %s" (plist-get my-plist :is-enabled) (plist-get my-plist :another-prop))
Отложенный запуск функций
(setq my-custom-timer (run-with-idle-timer 1 nil #'(lambda () (message "qwe"))))
Отложенные функции можно отменять
(cancel-timer my-custom-timer)
Операторы
Орпеторы это точно такие же функции. Вынес в отдельную категорию т.к. в большинстве языков это инструкции.
Детали
Switch case
(setq tt 'qwe)
(message "%s" (cond ((eq tt 'q2e) 1)
((eq tt 'oe) 2)
(t "qwe")))
While
(setq my-counter 0)
(while (< my-counter 12)
(setq my-counter (+ my-counter 1)))
(message "%s" my-counter)
Catch
Просто вау, в фп есть try catch! Я действительно удивлен..даже в объектно ориетированых языках это вызывает проблемы..тем не менее..это 1 из вариантов прерываия цикла while (плохихи вариатов, как по мне, но все же)
(setq my-counter 0)
(message "What is the messafe from catch? Oh this is message: %s" (catch 'result
(while (< my-counter 22)
(setq my-counter (+ my-counter 1))
(if (> my-counter 5)
(throw 'result "Amma result from catch block"))
)))
Return
Работает в emacs 27.1+. Позволяет прервать выполнение функции.
(setq my-counter 0)
(cl-defun my-iterator()
(while (< my-counter 12)
(if (> my-counter 3)
(return-from my-iterator)
)
(setq my-counter (+ my-counter 1)))
)
(my-iterator)
(message "%s" my-counter)
Взаимодействие с emacs
Детали
Вставка в текста
(insert "Hello" " " "World")
Работа с буфером
Программное создание нового буфера
(switch-to-buffer-other-window "*my-first-buffer*") (insert "Congratulations! I'am a new buffer")
Очистка буфера
(erase-buffer)
Интерактивный ввод
;; (read-from-minibuffer "Enter your name: ") (let ((your-name (read-from-minibuffer "Enter your name: "))) (switch-to-buffer-other-window "*Your personal info") (erase-buffer) (insert (format "Hello %s!" your-name)) (other-window 1))
Replace в буфере
(defun detect-bad-boys ()
(setq lesson-list '("Buzova" "Volodin" "Pupin"))
(defun mark-as-bad (name)
(insert (format "Bad boy %s \n" name)))
(switch-to-buffer-other-window "*lisp lesson*")
(mapcar 'mark-as-bad lesson-list)
(goto-char (point-min))
(while (search-forward "Bad")
(replace-match "Awful"))
(other-window 1)
)
(detect-bad-boys)
goto-char - переход к конкретному символу point-min - начало буфера
Добавление свойств для текста
Перед этим необходимо запустить предыдущую функцию
;; (detect-bad-boys)
(defun boldify-bad-boys ()
(switch-to-buffer-other-window "*lisp lesson*")
(goto-char (point-min))
(while (re-search-forward "Awful boy \\(.+\\)" nil t)
(message (format "Its %s" (match-beginning 1)))
(add-text-properties (match-beginning 1)
(match-end 1)
(list 'face 'bold-italic)))
;; (other-window 1)
)
(boldify-bad-boys)
Про сумасшедшие регекспы
;; The regular expression is “Bonjour \$.+$!” and it reads: ;; the string “Bonjour ”, and ;; a group of | this is the \$ ... $ construct ;; any character | this is the . ;; possibly repeated | this is the + ;; and the “!” string.
Создание кнопочки
Даннный метод создает кнопку над текстом с позиции от 1 до 10.
(defun button-pressed (button)
(message (format "Button pressed!")))
(define-button-type 'custom-button
'action 'button-pressed
'follow-link t
'help-echo "Click Button"
'help-args "test")
(make-button 1 10 :type 'custom-button)
Данная функция вставляет кнопку под текущей позицей каретки.
(insert-button "Press me"
'action (lambda (_arg) (print "You are press the button!")))
Чтение из completion
(completing-read "Choose one: " '("foo" "bar" "baz"))
Пользовательский ввод
(message "U say: %s" (read-string "Say me something: "))
Работа с выделенным текстом
Проверка что что-то выделено
(use-region-p)
Получить выделенный текст
(regionp (buffer-substring start end))
Конвертация символа в строку (ну и назад)
(symbol-name 'something) ;; Символ в строку
(intern (symbol-name 'something)) ;; Строка в символ
Overlay
Overlay это очень крутая тема. Он позволяет рендерить текст который не изменяет контент реального буфера. Это может быть полезно для показа подсказок, дебага, расчитанных значений.
Создание оверлея в конце строки
(setq my-first-overlay (make-overlay (line-end-position) (line-end-position)))
Курсор заходит за предел оверлея
В моем случае курсор выходил за предел оверлея. Решается весьма просто: вставляемый в оверлей текст необходимо наделить свойством
'cursor t
(setq my-popup-message (propertize popup-message 'face 'blamer--face 'cursor t))
Изменение свойств overlay
(overlay-put blamer--current-overlay 'after-string my-popup-message) (overlay-put blamer--current-overlay 'intangible t) (overlay-put blamer--current-overlay 'face 'bold) (overlay-put blamer--current-overlay 'cursor-intangible t)
Удаление существующего оверлея
(if my-first-overlay (delete-overlay my-first-overlay))
Создание своего minor-mode :WIP:
Работа с датами
(setq t3 (time-subtract (current-time) (days-to-time 2)))
(message "%s" (/ (float-time (time-since t3)) (* 60)))
Regexp
Примеры
Просто кучка примеров из разработанного мной пакета. Регекспы весьма похожи на то что представлено в других языках. Сложно лишь работать с интерполяцией строк (неочевидна работа с большим количеством слешей в исполняемом коде.)
(string-match "^\\([[:blank:]]\\)*\\(return\\)" " return {
name: 2
}")
(replace-regexp-in-string "[[:blank:]]*=[[:blank:]]*.+" "" " this.myVariable = somethingElse;")
(replace-regexp-in-string "\\(const\\|let\\|public\\|protected\\|private\\|var\\)[[:blank:]]*" "" "let anotherOne = userName")
Стандартные хуки
Window
Получение ширины текущего экрана
(window-total-width)
Создание своего пакета
Проверка ошибок компиляции
emacs -Q --batch \
--eval '(setq byte-compile-error-on-warn t)' \
-f batch-byte-compile turbo-log.el
Contribute
https://github.com/leotaku/elisp-check
CI
Пример github actions Про elisp check
Тесты
Тесты пишутся весьма просто. От части потому что не нужно мокать кучу зависимостей. Функция в большинстве случаев самодостаточна.
(ert-deftest my-first-test ()
(should (= (+ 10 10) 20)))
Запуск.
emacs -batch -l ert -l package.el -l test.el -f ert-run-tests-batch-and-exit