Функциональное программирование на Lisp: основные концепции и примеры

Lisp — это не просто язык, это атмосфера. Философия, заключённая в скобках, шепчущая приятные пустяки о лямбдах и замыканиях, попивая символический эспрессо. Давайте разберёмся, почему функциональное программирование на Lisp похоже на надевание перчатки суперсилы, украшенной рекурсивными драгоценностями и искрами высшего порядка.

Почему разработчикам на Lisp так весело

Lisp относится к функциям как к знаменитостям первого класса. Их можно:

  • передавать в качестве аргументов другим функциям;
  • возвращать в качестве значений;
  • хранить в структурах данных;
  • создавать динамически во время выполнения.

Это превращает кодирование из простого следования инструкциям в создание симфонии. Например, вот как можно передать функцию в качестве аргумента:

(defun apply-twice (fn x)
  (funcall fn (funcall fn x)))
(apply-twice (lambda (n) (* n 2)) 5)  ; Возвращает 20

lambda создаёт анонимную функцию, которая удваивает свой аргумент. Мы передаём эту функцию в apply-twice, которая применяет её дважды к числу 5 → 5×2=10 → 10×2=20. Элегантно, не правда ли?

Замыкания: секретный ингредиент

Замыкания в Lisp — это «таблетки памяти», которые запоминают среду своего рождения. Представьте счётчик, который приватно отслеживает своё состояние:

(defun make-counter ()
  (let ((count 0))
    (lambda () 
      (setf count (1+ count)))))
(defvar my-counter (make-counter))
(funcall my-counter) ; → 1
(funcall my_counter) ; → 2

Каждый раз, когда мы вызываем my-counter, он увеличивает свою приватную переменную count. Замыкание сохраняет доступ к count даже после выхода из make-counter.

graph LR A[make-counter] --> B[Создаёт count=0] B --> C[Возвращает lambda] C --> D[lambda запоминает count] D --> E[Увеличивает count при вызове]

Эта магия захвата среды обеспечивает логическую работу состояния без изменяемых глобальных переменных.

Функции высшего порядка: клей

Lisp-функции mapcar и reduce — это идеальные преобразователи данных. Давайте декодируем список строк с эмодзи:

(mapcar (lambda (s) 
          (length (remove-if-not #'alpha-char-p s)))
        '("🍕Пицца!" "🚀Ракетки!" "👾Пришельцы!"))
; → (5 7 6)

Здесь mapcar применяет нашу лямбду к каждой строке, подсчитывая только алфавитные символы. remove-if-not фильтрует неалфавитные символы — функциональное composition в действии!

Лексический и динамический области видимости: дуэль

Lisp в основном использует лексическую область видимости (переменные связываются там, где они определены), но также поддерживает динамическую область видимости. Сравните:

;; Лексическая область видимости
(let ((x 10))
  (defun show-x () x))
(let ((x 20)) (show-x)) ; → 10 (использует определение x)
;; Динамическая область видимости
(defvar *y* 30)
(defun show-y () *y*)
(let ((*y* 50)) (show-y)) ; → 50 (использует *y* во время вызова)

Соглашение *y* указывает на динамические переменные. Лексическая область видимости избегает неожиданностей — функции видят переменные из места их определения, а не из места вызова.

Функциональные шаблоны на практике

Шаблон 1: Пользовательские редукторы

Суммирование чётных чисел в списке:

(reduce #'+ 
        (remove-if-not #'evenp '(1 2 3 4 5 6)) 
        :initial-value 0)
; → 12

Шаблон 2: Генераторы функций

Создание умножителей:

(defun multiplier (n)
  (lambda (x) (* x n)))
(defvar double (multiplier 2))
(funcall double 8) ; → 16

Когда функциональное встречается с причудливым

Почему Lisp-функция рассталась со своей девушкой?
Потому что у неё было слишком много аргументов и она не могла «взять на себя обязательства»!
…Я ухожу.

Но если серьёзно — принятие функциональных парадигм в Lisp похоже на открытие того, что ваш код имеет настройку «композиция вместо инструкций». Ваши функции становятся многоразовыми блоками LEGO, замыкания управляют состоянием с элегантной защитой от амнезии, а функции высшего порядка превращают обработку данных в поэзию. Теперь вперёд, (funcall) свою креативность!