Программирование лучше осваивать на практических примерах. Чтобы пример был попроще - я взял несложную программу на языке программиирования бейсик из учебника1 и переписал программу для языка программирования LISP. Из функциональных языков сегодня моднее хаскель, но важно было разобраться с принципом. Чем программа на классическом функциональном языке программирования отличается от программы на классическом императивном языке программирования.
Начнём с базового понятия функционального программирования. Но нужно понимать, что к моменту написания этого поста я уже понимал основы функционального программирования, поэтому это базовые понятия, но не основы. Для чтения потребуется знать основы какого-либо функционального языка программирования и такие понятия, как лямбда-функции.
Функции более высокого порядка
Наша программа так и не избавилась от самого, на мой взгляд, существенного минуса первоначального варианта программы. Функция, которую мы дифференцируем, по-прежнему жёстко задана в тексте программы. Если мы хотим вычислить другую функцию — придётся править текст функции Euler. К счастью, среди возможностей языка LISP есть такая как функции более высокого порядка (есть и другие варианты перевода на русский, по-английски это higher order functions). Проще говоря, мы можем передавать в качестве аргументов (параметров) функции не только числа и строки, но и другие функции. Из такой возможности языка имеется множество следствий, но сейчас мы рассмотрим самый простой вариант использования функций более высокого порядка.
Для начала продемонстрирую, как это вообще работает. Вот совершенно бесполезный, но очень наглядный пример:
(defun HigherOrderFunction (y x)
(funcall y x))
Легко понять, что сделано. Определена функция с именем HigherOrderFunction, которая получает два аргумента. Необычно то, что она делает. Символ funcall применяет функцию, передаваемую ему в качестве первого аргумента к остальным аргументам. Можно продемонстрировать, что она делает ещё более простым примером. (funcall #'+ 1 2 3)
даст результат — 6. Знак ‘, стоящий перед выражением «+ 1 2 3» должен был «экранировать» выражение, пояснить интерпретатору LISP, что нужно принять выражение «как есть», а не вычислять его. Однако, funcall дал обратную инструкцию — применить аргументы 1, 2 и 3 к первому символу - «+».
Теперь можно воспользоваться нашей HigherOrderFunction.
(HigherOrderFunction (lambda (x) (+ x 2)) 12)
Результатом будет 14. Вместо y была передана функция без имени, лямбда-функция (x+2), в качестве второго аргумента передано число 12. HigherOrderFunction
применил первый аргумент в качестве функции и второй аргумент в качестве аргумента этой функции.
Теперь можно использовать это на практике. Переопределим функцию euler
таким образом, чтобы можно было передавать ей в качестве одного из аргументов — вычисляемую функцию.
(defun euler (y x dx xmax func)
;;;; approximate solution of differential equations of euler method
(if (<= x xmax)
(euler (+ (* (funcall func x) dx) y) (+ x dx) dx xmax func)
y
)
)
Теперь вызовем её.
(euler 1 1 0.1 2 (lambda (x) (* 2 x)))
Результат тот-же — 3,9. Но теперь можно передавать на вычисление любые функции с одним аргументом. Можно снять и это органичение — передавать функции от многих аргументов. Здесь это не нужно, метод Эйлера не предназначен для численного диференцирования функций от многих аргументов.
Функции более высокого порядка не являются уникальной особенностью LISP, они должны поддерживаться всеми другими функциональными языками программирования. Подобный трюк можно проделать и в некоторых версиях бейсика (я точно сделал бы это в ZX-Basic) и других интерпретируемых языках, например PHP. Правда там будут использованы немного другие механизмы, не функции более высокого порядка. Но чтобы сделать нечто подобное с языком программирования Си, C++ или другим, потребуется использование нетривиальных приёмов.
Побочные эффекты, как основной результат работы программы
Наша программа euler по-прежнему не делает одной вещи, которую выполняет программа на бейсике. Та выдаёт журнал своей работы. Строка PRINT x,y отчитывается о каждом цикле программы. Такие отчёты нужны для отладки программ, кроме того, они позволяют вести журналы вычислений и других действий.
Для выдачи таких отчётов в LISP-программе придётся также использовать свои механизмы, отличные от применяемых в императивных языках.
Идеальная функциональная программа вообще ничего не меняет в системе, она получает что-то на входе, обрабатывает эти строки, числа и списки по заложенному в неё алгоритму и выдаёт результат. Может быть это даже будет 42. Она не печатает результатов, не пишет на диск и даже не создаёт переменных, в идеальной функциональной программе даже нет таких понятий. Но в нашем материальном мире такие вещи нужны и LISP умеет обращаться к базам данных, запрашивать ввод от пользователя, писать на диск, использовать переменные и выводить данные на экран. Просто все эти действия называются побочными эффектами, вроде как такой добавкой к основному.
Наша программа euler
по-прежнему не делает одной вещи, которую выполняет программа на бейсике. Та выдаёт журнал своей работы. Строка PRINT x,y
отчитывается о каждом цикле программы. Такие отчёты нужны для отладки программ, кроме того, они позволяют вести журналы вычислений и других действий.
Для выдачи таких отчётов в LISP-программе придётся также использовать свои механизмы, отличные от применяемых в императивных языках.
Печать на экран — тоже побочный эффект. Для этого есть символ print, который выводит аргумент на экран, а перед ним делает перевод строки. И символы prin1 и princ, разница между ними для нас сейчас непренципиальна. Они просто выводят аргумент на экран, не делая перед ним перевода строки. Печать на экране для них побочное действие, в качестве основного они просто возвращают свой аргумент. Так что (+ 2 3)
просто посчитает выражение (и тоже напечатает его на экране, но это уже любезно сделает для нас интерпретатор лиспа), а (print (+ 2 3))
посчитает выражение и выведет результат на экран.
Зная это совсем просто переделать программу так, чтобы она отчитывалась о ходе вычислений:
(defun euler (y x dx xmax func)
;;;; approximate solution of differential equations of euler method
;;;; print log evry iteration
(if (<= x xmax)
(euler (print (+ (* (funcall func x) dx) y)) (princ (+ x dx)) dx xmax func)
y
)
)
Другие побочные эффекты от этой программы нам не нужны. Мы получили программу:
- Значительно более короткую, чем программа на бейсике. Её можно сократить до одной строки, но потеряем в читаемости кода.
- Более универсальную, она может численно дифференцировать любую, переданную ей функцию одного аргумента;
- Более наглядную. Хотя тут могут быть совершенно противоположные мнения, всё дело в привычке к чтению кода в функциональном стиле.
Как и чем работать с LISP-программами
В рассмотренных примерах я приводил код в самом тексте примера и ссылку на файл с исходным кодом, который можно загрузить и исполнить на своём локальном компьютере. Я думал, что читателям будет лучше самим найти оптимальную среду работы с программами на LISP, тем более, что их довольно много. Но потом вспомнил, сколько времени я сам искал оптимальную IDE для LISP и решил написать это запоздалое вступление.
Вообще, можно обойтись и без интегрированной среды для разработки программ - IDE. Все интерпретаторы лиспа умеют работать в режиме READ-EVAL-PRINT, когда они ждут ввода пользователя, потом выполняют введённый код и выводят на экран результат его выполнения. Таким образом вполне можно выполнять простые примеры. Проблема возникает тогда, когда требуется поправить код, в котором допущена ошибка. В некоторых реализациях LISP можно вызвать на редактирование существующее определение клавишами «вверх» и «вниз», где-то нельзя, но этот способ тем неудобнее. чем больше программа.
Я сам использую текстовый редактор emacs (который, за его многофункциональность, называют операционной системой для которой не хватает только драйверов) со средой SLIME. К этому варианту я пришёл не сразу, зато теперь едва ли откажусь от него.
Счастливым пользователям Linux установить SLIME просто, достаточно установить пакет SLIME (а он есть, наверное, в подавляющем большинстве дистрибьютивов Linux) и можно пользоваться. В случае неудачи поискать подсказку в интернете. Пользователям Windows сложнее, устанавливать SLIME и emacs придётся вручную, либо скачать готовый комплект LispBox. Последний вариант я и рекомендую. LispBox — модульная система, emacs с интегрированным в него SLIME скачивается отдельно, LISP отдельно, причём можно выбрать несколько разных вариантов лиспа. Я рекомендую common lisp.
О том, как работать с текстами в emacs имеется множество руководств, к ним я вас и отсылаю. Сама среда SLIME очень проста. На стадии знакомства с языком потребуется несколько пунктов меню (или несколько сочетаний клавиш им соответствующим). Это SLIME->Compilation->Compile Defun
, пункт меню или соответствующее сочетание клавиш стоит выбирать после задания очередного определения функции. И SLIME->Evaluation->Eval last expression, которое позволяет выполнить (вычислить) выражение после которого стоит курсор. Остальные пункты меню понадобятся по мере освоения среды, к SLIME прилагается весьма неплохая инструкция на английском языке (дать ссылку).
К недостаткам сборки emacs включённой LispBox
можно отнести отсутствие поддержки кириллицы. Это исправляется, но я стараюсь делать комментарии к программам на английском, что и вам советую.
Можно также попытаться использовать другие IDE для LISP.
Для кого предназначены эти материалы
Это второе запоздалое вступление к LISP. В первом материале я уже кратко написал, кому я адресую посты. Теперь опишу подробнее, мне поступают отзывы о том, что материалы одновременно “банальны” и “слишком сложны”.
Я пишу исключительно для любителей программирования, как я. Непрофессионалов.
Для всех профессионалов существуют отличные руководства по LISP. Их нетрудно найти в сети. Я могу порекомендовать следующие работы.
- COMMON LISP: A Gentle Introduction to Symbolic Computation. David S. Turetzky
- On Lisp, Paul Graham
- Мир лиспа, Э. Хювёнен, И. Сеппянен.
Переходим к реальному примеру
На с. 35 авторы книги наконец переходят к решению поставленной задачи об остывании чашки кофе. Они познакомили читателей с особенностями алгоритма численного дифференцирования Эйлера и теперь приводят программу для реального физического расчёта. Вот её листинг:
PROGRAM cool
CALL initial (t, temperature, room_temp, r, dt, ncalc)
CALL Euler (t, temperature, room_temp, r, dt, ncalc)
END
SUB initial (t, temperature, room_temp, r, dt, ncalc)
LET t=0
LET temperature=83
LET room_temp=22
LET r=0,1
LET dt=0,1
LET tmax=2
LET ncalc=tmax/dt
END SUB
SUB Euler (t, temperature, room_temp, r, dt, ncalc)
FOR icalc=1 TO ncalc
LET change=-r*(temperature-room_temp)
LET temperature=temperature+change*dt
LET t=t+dt
CALL output (t, temperature)
NEXT icalc
END SUB
SUB output (t, temperature)
PRINT t, temperature
END SUB
Я специально привёл этот листинг. Из него видно, что хотя название процедуры осталось прежним — Euler
, её текст существенно изменился. Авторы программы изменили названия переменных и несколько изменили алгоритм рассчёта, ведь скорость остывания зависит от температуры кофе и теперь имеем зависимость dt/dx=g(x,y)
, а старый алгоритм был предназначен для случая dt/dx=g(x)
. Но зачем переписывать процедуры, мы же и создавали её для того, чтобы потом использовать во всех случаях, когда потребуется численное дифференцирование!
Честно говоря, нашу функцию на лиспе тоже придётся немного изменить, но изменение будет самым несущественным, просто добавим дополнительную переменную, подставляемую в формулу, которую мы передаём для численного дифференцирования.
(defun euler (y x dx xmax func)
;;;; approximate solution of differential equations of euler method
(if (<= x xmax)
(euler (print (+ (* (funcall func x y) dx) y)) (princ (+ x dx)) dx xmax func)
y
)
)
Пришлось добавить всего одну букву, но зато эта же функция теперь не годится для дифференцирования функций, зависящих только от x. На самом деле — годится. В лиспе можно указывать, какие аргументы функции являются необязательными при помощи символа &optional за которым следует список необязательных аргументов. Так что просто добавим один символ в программу и продифференцируем одной функцией euler обе формулы.
(euler 1 1 0.1 2 (lambda (x &optional y) (* 2 x)))
(euler 83 0 0.1 15 (lambda (x y) (* (- y 22) -0.1)))
Так мы получили действительно универсальную функцию численного дифференцирования уравнений.
При желании можно выполнить те задания, что приведены1 на с. 39. Разбирать их здесь не буду, просто скажу, что большинство из них можно выполнить на LISP короче и красивее, чем на бейсике и, пожалуй, подавляющем большинстве других языков программирования.
Документируем программы
Раз уж мы можем писать на лиспе программы-функции, которые сможем потом использовать многократно, нужно хорошо их задокументировать. Так можно будет быстро вспомнить, что нужно передавать в функцию и что мы получим в качестве результата.
Обычно программы документируются при помощи комментариев в тексте и документации прикладываемой к программам. Часто документация составляется автоматически из комментариев в тексте. Такая возможность, например, предусмотрена в языке программирования Java. Кроме того, интерпретируемые языки программирования (сейчас граница между интерпретируемыми и компилируемыми языками программирования очень истончилась, не всегда и разберёшь) имеют такой замечательный способ получать справки, как интроспекция. По крайней мере в python и LISP эта возможность весьма удобна. Раз уж разработка программы всё равно диалог между программистом и средой языка, можно получать справки о функциях прямо в процессе этого диалога. Как это, например, делается с командами операционной системы. За подробной информацией по ним мы обратимся к руководству, но краткую справку, скорее всего, получим набрав в консоли имя команды —help или имя команды /? как это приходилось делать в MS DOS.
В лиспе можно дать информацию для интроспекции разместив краткую справку в строке сразу за определением новой функции в двойных кавычках.
Использовав функцию (documentation 'foo 'function)
можно будет освежить в памяти параметры, с которыми вызывается программа