ONE DUDE`S BLOG

/media/elisp.webp

Elisp.

20.09.2021
(message "Язык состоящий на 30% из смайликов").

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"))
  1. Получить первый элемент

    (car my-first-list)
  2. Получить все кроме первого элемента..

    (cdr my-first-list)
  3. Добавить элемент в список

    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)
  4. Слияние 2 списков

    (setq my-first-list '(?q ?b ?c))
    (setq my-first-list (append my-first-list (list ?t)))
    (message "%s" my-first-list)
  5. Map

    На самом деле mapcar (возможно создатель языка хотел иметь машину…).

      (defun greeting (name)
        (format "Hello %s" name))
    
      (mapcar 'greeting my-first-list)
  6. forEach

    mpcar создает новый список, можно просто итерироваться по записям с помощь. dolist

    (let* ((v ""))
    
      (dolist (p '("one" "two" "three"))
        (setq v (concat v " " p)))
      (message v))
  7. Проверить есть ли элемент в списке

    (member "123" '(1233 "qwe" "123"))
  8. Перезаписать элемент в списке по индексу

    (setq my-test-list '(("qwe" . 1) ("be" . 2)))
    (setcdr (assoc "qwe" my-test-list) "asdlkajsdakd")
    (message "%s" my-test-list)

Ассоциативные массивы

  1. Объявление

    (setq trees '((a . 1) (b . "qwe")))

    При чем точка нужна для специального типа symbols. Если работает с реальными значениями то можно и без нее

    (setq another-hashmap '(("a" "First elem") ("b" "Second elem")))
  2. Получить элемент по ключу

    (message "%s" (assoc 'a trees))

    Ну и конечно возвращает оно кортеж..а чтобы получить элемент нужно использовать уже известную нам функцию - cdr

    (message "%s" (cdr (assoc 'a trees)))
  3. Получить элемент по значению

    (message "%s" (rassoc "qwe" trees))

    При этом rassoc работает и для строк и для чисел, а вот rassq только для чисел

    (message "%s" (rassq "qwe" trees)) ;; nil
    (message "%s" (rassq 1 trees)) ;; (a . 1)
  4. Копирование мапы

      (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)
  5. Удаление всех записей по ключу

      (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)))
  6. Удаление записей по значению

      (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")

Работа с буфером

  1. Программное создание нового буфера

      (switch-to-buffer-other-window "*my-first-buffer*")
      (insert "Congratulations! I'am a new buffer")
  2. Очистка буфера

    (erase-buffer)
  3. Интерактивный ввод

      ;; (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: "))

Работа с выделенным текстом

  1. Проверка что что-то выделено

    (use-region-p)

  2. Получить выделенный текст

    (regionp (buffer-substring start end))

Конвертация символа в строку (ну и назад)

(symbol-name 'something) ;; Символ в строку
(intern (symbol-name 'something)) ;; Строка в символ

Overlay

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

  1. Создание оверлея в конце строки

    (setq my-first-overlay (make-overlay (line-end-position) (line-end-position)))
  2. Курсор заходит за предел оверлея

    В моем случае курсор выходил за предел оверлея. Решается весьма просто: вставляемый в оверлей текст необходимо наделить свойством 'cursor t

    (setq my-popup-message (propertize popup-message 'face 'blamer--face 'cursor t))
  3. Изменение свойств 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)
  4. Удаление существующего оверлея

    (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
emacs
elisp
емакс
0
174