Функциональное программирование на 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
.
Эта магия захвата среды обеспечивает логическую работу состояния без изменяемых глобальных переменных.
Функции высшего порядка: клей
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)
свою креативность!