newLISP для программистов

v. 1.1

Эта статья адресована программистам на "традиционных" процедурных и объектно-ориентированных языках. Ее цель - показать некоторые удобные приемы программирования, обычные для функционального языка LISP, но редко используемые в других языках.

ЛИСП - это целое семейство диалектов, наиболее известным и признанным среди которых является Common LISP. Однако, основой для этой статьи послужил упрощенный диалект newLISP, интрепретатор которого работает в любой операционной системе и который можно использовать для "повседневных задач" на уровне языка Perl. Различия между newLISP и Common LISP в рамках излагаемого материала абсолютно не существенны.

О том, где взять дистрибутив newLISP написано в последней главе.

Итак... попробуем!

Для чего нужен ЛИСП?

Большинство языков программирования (за исключением BASICа ;-) разрабатывались с целью упрощения решения определенных задач. Название языка LISP расшифровывается как "LISt Processor", "обработчик списков" - это и есть основная область его применения.

При записи на ЛИСПе, список ограничивается круглыми скобками, а его элементы разделяются пробелами.
Какие данные может содержать список? - в ЛИСПе - практически любые! Элементами списка могут быть константы, имена переменных и функций (в терминологии ЛИСПа - символы) и, конечно, другие списки:

(1 2 "abc" var (34 "w"))
Можно без преувеличения сказать, что список в ЛИСПе - основной структурный и аггрегирующий тип данных. Записи - аналогои struct или record в Common LISP реализованы средствами самого языка, а в newLISP-е вообще отсутствуют. Современные реализации (включая и newLISP) имеют встроенную поддержку массивов и хэшей, однако реально они используются только в случаях, когда для алгоритма действительно нужны массивы или хэши, т.е., как ни удивительно, достаточно редко. Следует отметить, что вместо хэшей в ЛИСПе обычно используются "ассоциативные списки".

Ассоциативный список - это "список списков", в которых первый элемент используется как ключ для поиска:

((ключ1 значение1_1 значение1_2) (ключ2 значение2_1 значение2_2) .....)
Для поиска в ассоциативных списках используются функции "assoc" и "lookup". С одной стороны, такой подход (теоретически) приводит к снижению производительности поиска (на практике - это вопрос спорный...), зато с другой - вы можете иметь несколько элментов с одинаковым ключом и у вас всегда фиксирован порядок следования записей.

(Как?
  (читать лисп-программу))

Очень просто - все, что находится вутри круглых скобок - это вызов функции, причем первое слово после открывающей скобки - это имя функции, а остальные - ее параметры.
То, что математик записал бы:
f1(x, y)
на ЛИСПЕ будет выглядеть как:
(f1 x y)
А более сложная запись:
f1(x, f2(y, z))
превратится в:
(f1 x (f2 y z))
Пример, сложение:
"Математическая" запись:
1 + 2 -> 3
ЛИСП-запись:
(+ 1 2) -> 3
(здесь и далее знак "->" будет предварять результат вычисления выражения)

Скажете "Неудобно!"? - Тогда посмотрите на это:

(+ 1 2 3 4 5) -> 15
Далее будут продемонстрированы и более интересные следствия такого подхода.

Как вычисляется ЛИСП-выражение?

Так же, как и в математике: при вычислении функции сначала производится вычисление ее аргуметов, а за тем - над ними производятся действия, определяемые данной функций.
(+ 10 (sqrt 25)) -> 15
функция "sqrt" (квадратный корень) вычисляется, результат (5) передается функции "+", которая, в свою очередь, вычисляется и возвращает результат: 10 + 5 = 15.

Несколько полезных примеров:

(setq a "test") -> "test"
"setq" - функция присваивания, она возвращает значение последнего аргумента, но также присваивает символу "a" значение "test".
a -> "test"
вычисление символа возврщает его значение.
(setq b (sqrt 25)) -> 5
без комментариев
(setq b '(sqrt 25)) -> (sqrt 25)
апостроф - специальный символ, предотвращающий вычисление выражения, результат - обычный список. На самом деле, апостроф - это просто короткая запись для функции "quote". Единственное расширение чисто-скобочного ЛИСП-синтаксиса, которое введено в синтаксис в связи с частым использованием:
(setq b (quote (sqrt 25))) -> (sqrt 25)
"quote", в свою очередь - простейший пример "макро-функции" - специальной функции, чьи аргументы автоматически не вычисляются. Мы еще вернемся к различным способам применения макро-функций.
(setq b '(("first" 1) ("second" 2) ("third" 3)))
  -> (("first" 1) ("second" 2) ("third" 3))

(b 1) -> ("second" 2)
получение элемента по индексу, первый элемент имеет индекс 0. Такой синтаксис в newLISP называется "implicit indexing".
(примечание: в Common LISP такого синтаксиса нет, индексация осуществляется функцией "nth").
(1 b) -> (("second" 2) ("third" 3))
срез со второго элемента и до конца списка.
(0 2 b) -> (("first" 1) ("second" 2))
срез с первого элемента списка длиной 2 элемента.
Есть и более традиционный для ЛИСПа способ - функции "nth" (элемент по индексу) и "slice" - срез.
(assoc "second" b) -> ("second" 2)
(lookup "second" b) -> 2
(lookup "second" b 0) -> "second"
(lookup "second" b 1) -> 2
поиск в ассоциативном списке: "assoc" возвращает весь подсписок, а "lookup" - последний элемент подсписка, либо элемент с индексом, указанным в третьем параметре.

(К-чему?
  (все
    (эти скобки)))

Код программы на ЛИСПе выглядит примерно так же, как название этой главы. Конечно, сразу в глаза бросается самое важное - обилие круглых скобок ;-) И первый вопрос, который обычно возникает при знакомстве с ЛИСПом - зачем использовать столько скобок, когда их можно заменить таким же количеством запятых, точек с запятой, фигурных скобок и прочих удобнейший синтаксических рюшечек?

Ответ лежит на поверхности: Вы, вероятно, уже обратили внимание на внешнее сходство ЛИСП-списков и ЛИСП-выражений. Действительно, ЛИСП-выражение синтаксически является ЛИСП-списком. Более того, оно может обрабатываться как обычный список - храниться в переменных, подвергаться преобразованиям, передаваться параметром функции в виде списка, и, конечно же - выполнятся!

Это - одно из основополагающих свойств ЛИСПа - использование кода, как данных.

Несложная программа в качестве примера: случайное перемещение по четырем направлениям

(setq x 0 y 0) ; инициализация позиции

(define (up moves) (dec 'y moves))
; определение функции с именем "up", параметром "moves", которая уменьшает
; значение символа "y" на величину "moves"

(define (down moves) (inc 'y moves)) ; аналогично
(define (left moves) (dec 'x moves))
(define (right moves) (inc 'x moves))

(setq doings (list up down left right))
; всего лишь список символов - только что определенных функций ;-)
; функция list - создает список

(seed (date-value)) ; инициализируем датчик случайных чисел

; непосредственно выржение перемещения
(dotimes (i 100)
 ((doings (rand 4)) (rand 5))
 (println x ":" y))
Разберем выражение перемещения подробнее:

(dotimes (i 100) выражение1 выражение2 ...) - вычисляет "выражения" 100 раз, при этом символ "i" принимает значения от 0 до 99.

(rand 4) - генерирует случайное целое в диапазоне от 0 до 3.

(doings (rand 4)) - эта форма была описана в предыдущей главе, как "получение элемента списка по индексу". Соответственно: "doings" - список, а "(rand 4)" служит индексом.

Здесь стоит посмотреть на список doings более внимательно:

doings ->
  ((lambda (moves) (dec 'y moves))
   (lambda (moves) (inc 'y moves))
   (lambda (moves) (dec 'x moves))
   (lambda (moves) (inc 'x moves)))
Как и было обещано в начале главы - обычный список может содержать код программы! Слово "lambda", которое стоит в начале каждой функции - элемента doings, не является элементом списка, а обозначает, что данный список - функция, которая может быть вызвана. Такие функции называются "lambda-функциями", или "безымянными функциями". В то же время лямбда-функция - это обычный список, и первым (нулевым) элементом элементом наших лямбда-списков является элемент "(moves)".

Возможно, у вас возник вопрос: а как же имена - "up", "down", "left" и "right"? На самом деле, эти имена всего лишь символы-переменные, которым присвоены в качестве значений соответствующие лямбда-списки:

up -> (lambda (moves) (dec 'y moves))
(up 1) -> -1            ; происходит вызов функции с параметром 1
(nth 0 up) -> (moves)   ; поскольку implicit indexing здесь не работает,
                        ; используем функцию nth для получения 0-го элемента
(setq up-new up)
up-new -> (lambda (moves) (dec 'y moves))
(up-new 1) -> -2        ; было -1, уменьшаем еще на единицу...
Фактически, следующие два выражения идентичны:
(define (up moves) (dec 'y moves))
(setq up '(lambda (moves) (dec 'y moves)))
Однако, двинемся далее с нашим примером...

((doings (rand4)) (rand 5)) - поскольку элемент списка "doings" - это функция, мы можем использовать ее как обычный вызов функции - т.е., подставить в ЛИСП-выражение вместо имени функции. Соответственно, случайное число (rand 5) будет аргументом функции.

(println выражение1 выражение2 ...) - печатает на экран результаты вычисления выражений-аргументов, завершая их переводом строки. Кроме того, функция "println" возвращает в качестве результата значение вычисления последнего выражения-аргумента.

В заключение разбора данного примера следует отметить, что с не меньшим успехом мы могли бы составить список "doings" не из кода функицй, а из символов, которым они присвоены:

.....
(setq doings '(up down left right))
.....
(dotimes (i 100)
 (apply (doings (rand 4)) (list (rand 5)))
 (println x ":" y))
Единственная новая функция здесь - "apply" - первый ее аргумент - имя функции, которую она должна вызвать, а второй - список аргументов, с которыми должна быть вызвана эта функция.

Возможности, подобные описанным частично и рудиментарно присутствуют в процедурных языках, однако они используются, в основном, Очень Крутыми Программистами в минуты отчаяния. Чтобы сделать их более дружественными, в процедурные языки пришлось добавить ООП, которое, в свою очередь, потребовало раздутых объектных декомпозиций и внесло порядочную неразбериху в освоение программирования.

Однако в ЛИСПе использование кода как данных является "повседневной" практикой, применяемой при любой необходимости (а также и вовсе без оной ;-).

В завершение главы - пару слов об ООП в ЛИСП:
В языке Common LISP система объектно-ориентированного программирования (CLOS) реализована как обычная библиотека, написанная на том же Common LISP - без каких-либо изменений самого языка.
В языке newLISP для программирования с объектами предлагается встроенная система "контекстов" ("context") - изолированных пространств имен, реализующих основные принципы ООП в форме, максимально приспособленной для быстрого "скриптинга".

Исключения, подтверждающие правила.

В предыдущих главах мы уже встречались с необычным исключением из стройного скобочного ЛИСП-синтаксиса - символом апострофа, сокращенной записи для макро-функции "quote". Действительно, эта функция - единственный способ в ЛИСП задавать в качестве аргумента функции список-константу. Что делает ее такой особенной?
(define (test1 arg) (println arg))
; определим обычную функцию
(tes1 (+ 1 2)) -> 3
; и испробуем...

(define-macro (test2 arg) (println arg))
; теперь определим "необычную" макро-функцию
(test2 (+ 1 2)) -> (+ 1 2)
; и испробуем...
В отличие от обычных функций, аргументы которых автоматически вычисляются перед вызовом, при вызове "макро-функций" используется так называемый "ленивый" (lazy) порядок вычисления, в котором аргументы не вычисляются вообще.

Во избежание лишней путаницы, здесь стоит заметить, что слово "макро" в ЛИСПе не означает, что функция вычисляется препроцессором до интерпретации (компиляции) основного кода, как это происходит в языке Си. "Макро"-функции, так же как и обычные, вычисляются непосредственно во время выполнения программы.

Функция "quote" является простейшей макро-функцией. Если бы она отсутствовала в языке, ее можно было бы определить так:

(define-macro (quote a) a)
; функция принимает один аргумент
; и возвращает его в неизменном виде (без вычисления) в качестве результата.

Если вы передаете аргументом макро-функции ЛИСП-выражение, то это выражение само по себе и будет доступно в качестве переменной при вычислении вызываемоой макро-функции. Если вы захотите его вычислить, вам понадобится функция "eval":

(define-macro (test3 arg) (println (eval arg)))
(test3 (+ 1 2) -> 3
Такой фокус порождает интересную возможность:
(define-macro (my-if condition result-true result-false)
  (let (_c (eval condition))
    (and _c (eval result-true))
    (or _c (eval result-false))))

(my-if (= 1 1) (println "ture") (println "false")) -> "true"
(my-if (= 1 2) (println "ture") (println "false")) -> "false"
Функция "let" имеет синтаксис:
(let (символ1 выражение-значение1 символ2 выражение-значение2 ...)
  выражение-действие
  выражение-действие
  ....)
она создает и инициализирует символы, которые будут действовать в момент вычисления выражений-действий, и прекратят свое существование (освободят память) в момент возврата из функции "let". Результатом функции "let" является результат вычисления последнего выражения-действия.

Функции "and" и "or" - логические и действуют по очевидной схеме:
Для "or" - если первое выражение ложь, вычисляется второе и т.д., возвращается результат первго истинного выражения.
Для "and" - соответственно.

Таким образом, мы незаметно определили и испробовали новую синтаксическую конструкцию - самый обычный оператор условного ветвления! И самое удивительное в этом операторе то, что внешне он не отличим от любых, в том числе встроенных, синтаксических конструкций ЛИСПа!
Благодаря этой возможности ЛИСП по праву называется "мета-языком", или "языком для создания языков": одним из рекомендуемых методов программирования на ЛИСПе является создание собственного языка с синтаксисом, удобным для решения определенной задачи, а затем использование этого языка для получения требуемого результата.

К слову сказать, при создании компиляторов Common LISP большая часть стандартного синтаксиса языка реализуется не на языке создания компилятора, а на самом же языке Common LISP в качестве библиотек.

Конечно, есть и более сложные и продуктивные способы построения макро-функций, основанные на модификации передаваемого им кода и составления на его основе новых выражений. При написании макросов на newLISP не забудьте познакомится с функцией "letex".

Немного об условных вычислениях.

Раз уж мы помянули условный оператор, давайте взглянем поближе на логические вычисления в newLISP (традиционный ЛИСП имеет некоторые отличия, в частности, другую трактовку понятия "nil" и весьма необычные правила равенства).

Основа логических вычислений, двоичная логика, построена на значениях "истина" и "ложь". В newLISP в качестве значения "ложь" используется символ "nil". Второе назначение этого символа - "пустое" значение, которое имеют не инициализированные символы.

Все остальные значения в newLISP трактуются как "истина" (в т.ч., 0, пустая строка и пустой список). Для удобства написания программ в newLISP имеется также специальный символ "true" - его могут возвращать некоторые логические функции.

(if условие выражение
    условие выражение
    ....
    иначе-выражение)
Так выглдит настоящая функция условного ветвления. Если "условие" истинно, выполняется соответствующее "выражение" и вычисление функции прерывается. Если все "условия" ложные - выполняется "иначе-выражение". Если какое-либо "выражение" вычислялось, его результат возвращается в качестве результата функции, иначе возвращается nil.

Обратите внимание, что "выражением" может быть только одиночное выражение:

(if вправо (+ x 1))
Если необходимо выполнить последовательность из нескольких выражений, их нужно "обернуть" функцией "begin":
(if вправо-вниз (begin
                  (+ x 1)
                  (+ y 1)))
"begin" - простая функция для связывания последовательности выражений. Конечно, при необходимости, вместо нее можно использовать уже знакомую нам функцию "let", другой условный оператор или еще что-нибудь, подходящее случаю.

Обратите внимание, что в примере использованы "кириллические" имена символов "вправо" и "вправо-вниз" - в ЛИСПе это допустимо.

Альтернативный вариант функции "if" - функция "cond":

(cond (условие выражения)
      (условие выражения)
      ...)
За счет дополнительных скобок "выражения" могут быть последовательностью нескольких выражений без дополнительных "оберток".

И, наконец, венец функций условного ветвления - функция "case":

(case символ
  (тестовая-константа выражения)
  (тестовая-константа выражения)
  ...
  (true выражения))
Пример:
(define (translate n)
  (case n
    (1 "one")
    (2 "two")          
    (3 "three")
    (4 "four")
    (true "Can't translate this")))

(translate 3)  -> "three"
(translate 10) -> "Can't translate this"
В этом примере значение символа "n" будет последовательно сравниваться с "тестовыми константами" 1, 2, 3 и т.д. и, при совпадении, будет вычислено соответствующие выражение. Обратите внимание, что в этом примере в качестве результирующих выражений используются строковые константы.
Тестовая константа "true" используется для обозначения действия по-умолчанию.

Теперь, с помощью функции "translate" численное значение может быть преобразовано в слово-числительное.

Следует отметить, что у функции "case" есть особеннось: "тестовые константы" - это именно константы, они не могут быть вычисляемыми выражениями. Т.е., данная запись синтаксически допустима, но не принесет желемого результата:

(case n
  (a "n равно знчению a")
  ((+ a 1) "n на единицу больше значения a"))
Преимуществом такого поведения является повышенная (оптимизированная) скорость работы функции "case".

Но как быть, если удобство важнее? Очень просто - написать свой макрос:

(define-macro (ecase _v)
  (eval (append
          (list 'case _v)
          (map (fn (_i) (set-nth 0 _i (eval (_i 0))))
               (args)))))
Испробуем:
(setq a 1 n 2)
; присваиваем a=1, n=2

(ecase n
  (a "n равно знчению a")
  ((+ a 1) "n на единицу больше значения a"))

  -> "n на единицу больше значения a"
Макро-функция "ecase" работает следующим образом: В списке своих аргументов, таком же, как для "case", она заменяет все выражения, стоящие на местах "тестовых констант" на результат их вычисления - т.е. на константы.
Затем в начало преобразованного списка добавляется символ "case" и имя тестируемой переменной.
Затем получившееся выражение вычисляется с помощью функции "eval".

Текст данного макроса пока что немного сложен - он станет понятен после глав "Преобразование списков" и "Анонимные функции".

Такая "неудобная" функцональная запись.

Простое выражение:
a = 1 + 2;
на ЛИСПе можно записать как:
(setq a (+ 1 2)) -> 3
Выглядит непривычно? Удобство записи не очевидно?
Тогда поглядите на это:
(setq a (+ 1 2 3 4 5)) -> 15
Многие из стандартных функций ЛИСПа могут обработать произвольное количество аргументов!
Тут, конечно, можно сказать, что не велико пробретение - возможность статически указать произвольный список аргументов. Однако, не спешите...
Легко заметить, что аргументы функции "+" являются списком. Запишем его:
(setq l '(1 2 3 4 5)) -> (1 2 3 4 5)

l -> (1 2 3 4 5)
Теперь у нас есть список. Если бы мы могли сконструировать выражение из требуемого имени функции и нашего списка, и вычислить его, то обработка функциями произвольного количества аргументов получила бы больше смысла...
(define-macro (my-apply fun lst)
  (eval (cons fun (eval lst))))

(my-apply + l) -> 15
Новая для нас функция "cons" создает список, добавляя новый элемент (первый аргумент) в начало существующего (второй аргумент). На самом деле, мы уже знакомы со встроенной макро-фукцией "apply", которая делает то же, что и наш макрос.
(setq a (apply + l)) -> 15
Функция "apply" вычисляет функцию, указанную первым аргументом, передавая ей в качестве аргументов список, указанный вторым аргументом.
Конечно, на месте функции "+" может быть любая другая функция, в том числе и пользовательская:
(define (average) (div (apply add (args)) (length (args))))
  -> (lambda () (div (apply add (args)) (length (args))))

(apply average l) -> 3
"add" и "div" - аналоги функций "+" и "/", но работающие с числами с плавающей точкой.
"args" - функция, возвращающая все несвязанные аргументы, переданные функции.
Т.е.:
(define (f x y) (println "x=" x " y=" y " args=" (args)))
  -> (lambda (x y) (println "x=" x " y=" y " args=" (args)))

(f 1 2 3 4) -> x=1 y=2 args=(3 4)

Анонимные функции.

Функций, которые, подобно "apply", принимают в качестве аргументов другие функции, в ЛИСПе довольно много. Вы даже можете написать свои собственные.

Одним из наиболее замечательных встроенных экземпляров является функция "map".

(map pow '(1 2 3 4)) -> (1 4 9 16)
; pow - возведение в квадрат

(map first '((1 2 3) (4 5 6) (7 8 9))) -> '(1 4 7)
; first - возвращает первый элемент списка
Функция "map" служит для преобразования элементов списка. Функция, указанная в первом аргументе "map" последовательно применяется ко всем элементам списка, указанного во втором аргументе. Результаты вычисления составляются в новый список, который и возвращается функцией "map".
Однако, не все здесь кажется удобным:
(define (third lst) (lst 2))
; функции "third" в языке нет

(map third '((1 2 3) (4 5 6) (7 8 9))) -> '(3 6 9)
Мы всего лишь хотели взять третий элемент каждого подсписка, а для этого нам пришлось предварительно определять функцию. Хотя иногда это бывает совсем не вредно, в общем случае, хотелось бы иметь возможность без этого обойтись. На помощь приходит функция "fn" - конструктор анонимных лямбда-списков:
(map (fn (lst) (lst 2))
     '((1 2 3) (4 5 6) (7 8 9))) -> '(3 6 9)
Работа функции "fn" аналогична функции "define", за исключением того, что "fn" не осуществляет присваивание созданного лямбда-списка какому-либо символу:
(define (third lst) (lst 2)) -> (lambda (lst) (lst 2))
; кроме того, символ "third" получил значение - этот же лямбда-список

(fn (lst) (lst 2)) -> (lambda (lst) (lst 2))
; только лямбда список в результате; никакого дополнительного эффекта
В пару к функции "fn" существует "fn-macro", предназначенная для создания анонимных макросов.

Мы уже видели подобную технику в главе "(К-чему? (все (эти скобки)))", только в ее примерах использовалась не функция-конструктор "fn", а непосредственно готовый лямбда-список, защищенный символом апострофа:

(fn (lst) (lst 2)) -> (lambda (lst) (lst 2))
'(lambda (lst) (lst 2)) -> (lambda (lst) (lst 2))
По результату, эти выражения эквивалентны. Разница лишь в краткости записи.

Преобразование списков.

В примере с макросом "ecase" мы уже встречали функцию "list", которая позволяет создавать списки из отдельных элементов:
(setq a 25)
(list 1 2 3 a) -> (1 2 3 25)
Там же мы видели и функцию "append", которая объединяет списки:
(append '(1 2 3) '(4 5 6)) -> (1 2 3 4 5 6)
А в примере с "my-append" - функцию "cons", которая добавляет элемент в начало списка:
(cons 1 '(2 3 4)) -> (1 2 3 4)
(cons '(1 2) '(3 4)) -> ((1 2) 3 4)
В традиционном ЛИСПе, где списки программно представлены в виде "головы" и "хвоста", "cons" играет гораздо более существенную роль, объединяя эти два компонента. В newLISP списки строятся по "линейному" принципу.

Гораздо больший интерес представляют функции, предназначенные для преобразования одного списка в другой. Наиболее простой и ожидаемой является функция "filter", которая фильтрует список, оставляя в нем только значения, удовлетворяющие заданному условию:

; (filter тестовая_функция список)

(filter (fn (x) (not (empty? x))) '("abc" "" "def" "jhi" ""))
  -> ("abc" "def" "jhi")
"empty?" - функция возвращающая "true", если ее аргумент - не пустая строка и не пустой список.
"filter" возвращает список, в котором остаются только те элементы, для которых тестовая функция вернет истину.

Обратной по действию является функция "clean":

(clean empty? '("abc" "" "def" "jhi" "")) -> ("abc" "def" "jhi")
Еще одна простая, но полезная функция - "join" - объединяет список строк в одну строку.
(join '("abc" "" "def" "jhi") ":") -> "abc::def:jhi"
(join '("abc" 123 "def" "jhi") ":") -> Ошибка! - 123 не является строкой
(join (map string '("abc" 123 "def" "jhi")) ":") -> "abc:123:def:jhi"
Первый параметр "join" - список строк, второй (опциональный) - строка-разделитель.
Использованная функция "string" преобразует любой тип данных в текстовое представление.

И, конечно, самой "чудесной" является рассмотренная в предыдущей главе функция "map". В дополнение скажем, что она может обрабатывать несколько списков.

(map (fn (x y) (+ x y)) '(1 2 3 4) '(5 6 7 8)) -> (6 8 10 12)
А, учитывая известное свойство "+", можно даже проще:
(map + '(1 2 3 4) '(5 6 7 8)) -> (6 8 10 12)
Теперь вы знете достаточно, чтобы вернуться к макросу "ecase" и увидеть, как он работает.

Методы программирования.

Если вооружась навыками процедурного программирования вы попробуете писать на ЛИСПе (и, тем более, на newLISP-е), то довольно скоро преуспеете. Однако врядли получившийся результат будет чем-то превосходить аналоги на обычных языках. ЛИСП - функциональный язык, в нем действуют свои законы эффективности и найдены свои методы комфортного программирования.

Самодостаточные функции.

"Чистое", академически правильное функциональное программирование требует, чтобы используемые функции не имели "сторонних эффектов". Т.е., функция может обрабатывать только те данные, которые получает в качестве параметров и единственным результатом ее работы должно быть возвращаемое значение. Функция не должна использовать или изменять какие-либо "глобальные" переменные.

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

Функциональная запись.

Сокращение использования сторонних эффектов позволяет, в свою очередь, увеличить возможности функциональной записи - то, что в процедурном языке обычно записывается в виде нескольких выражений, в ЛИСПе зачастую можно уложить в одно. При этом вложенная структура ЛИСП-выражения хорошо выявляет логические связи его компонентов. По внешнему виду функциональная запись более похожа на естественный язык, в то время, как процедурная - ни что иное, как "причесанное" наследие ассемблера.

Разработка снизу - вверх.

Конечно, не стоит ожидать "прозрачности" от функционального выражения длиной в две страницы. Всем известно, что слишком большой код полезно дробить на функции. Обычный для процедурных языков подход - определить задачу, выделить подзадачи, и дробить их пока не станет возможным написать для них отдельные функции. Функции все равно получаются большие и сложные, а к концу разработки приходит понимание, что надо было решать немного другую задачу... Такой способ называется разработкой "сверху-вниз".

В ЛИСПе, благодаря его свойствам мета-языка, все делается наоборот. Для решения задачи исследуется предметная область и на основе возможностей ЛИСПа создается новый язык. Затем на этом языке записывается решение требуемой задачи. Если "вдруг" выясняется, что первоначальный план претерпел существенные изменения, не беда - на уже готовом языке несложно записать и что-то новое. Этот способ называется разработкой "снизу-вверх".

Особо приятным следствием разработки "снизу-вверх" является возможность на каждом уровне решения задачи использовать наиболее подходящий язык, опять же, легко транслируемый в логику естественного языка.

Самодокументирование.

Ко всему выше сказанному следует добавить два замечания:
- не скупитесь на "самодокументирующие" имена функций и переменных. Имена "t", "tmp", "ex" и т.п. хороши в локальных участках кода. Однако для глобальных имен что-нибудь вроде "exit-state" подойдет гораздо лучше (и даже не говорите, что вы медленно печатаете!)
- иногда и одноразовое действие лучше оформить в виде отдельной функции.

Не лишайте себя возможности писать так:

(do-select ((CustomerName CustomerEmail)
            :from Customers
            :where (> CustomerAge 100))
  (send-email CustomerEmail
              :subject "Congratulations!"
              :body (format nil
                            "Dear ~A, you won a prize! Call ~A."
                            CustomerName company-phone)))
Комментарии излишни, не правда ли?
Этот пример написан в синтаксисе Common Lisp. Выловлен из флейма на http://linux.org.ru.

Обработка списков.

И, наконец, помните, что ЛИСП создан для обработки списков. Чем больше возможностей для применения списков вы увидите в вашей задаче, тем больше возможностей по обработке ваших данных вы получите "на халяву" ;-)

Форматирование кода.

Существуют разные мнения о том, как лучше форматировать код на ЛИСПе и newLISP-е. Здесь изложен достаточно удобный "канонический" метод:

Отступы.

Обычный отступ для обозначения вложенности устанавливается равным двум пробелам.

Отступ для вложенных списков данных - один пробел (поскольку открывающие скобки на предыдущей строчке могут следовать одна за другой).

Если у функции много аргументов и первый находится на одной строке с именем функции, остальные аргументы удобно писать под первым "в столбик" (как в предидущем примере).

Для специальных функций, вроде "let" или "if", у которых первый параметр имеет особое значение, первый параметр обычно начинают на одной строке с именем функции, а остальные - на других строках со стандартным отступом в два пробела:

(if (= a b)
  (println "равенство")
  (println "неравенство"))

Закрывающие скобки.

Закрывающие скобки обычно не выносят на отдельные строки, как это делается в языке "Си" и подобных. Это бессмысленно для выделения структуры, поскольку отступы и так несут всю необходимую информацию.

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

В отсутствие "правильного" редактора, скобки удобно закрывать "вслепую" - глазами считать отступы кода с самого внутреннего до уровня, который надо завершить, и каждый раз вслепую нажимать закрывающую скобку на клавиатуре.

Практический скриптинг.

Самой общеупотребительной задачей скриптинга является, конечно же, обработка текстовых отчетов. Безусловным лидером по удобству здесь является AWK - нет языка, на котором можно было бы писать лаконичнее. К сожалению, это довольно слабый язык, быстро сдающий позиции, когда возникает необходимость в сложной обработке данных. В этой области уже традиционно главенствует PERL.

Для тех же задач неплохо подходит и newLISP. Этот язык также поддерживает Perl-совместимые регулярные выражения PCRE и позволяет производить разбор текстовых документов. Особенностью применения newLisp является его ориентация на обработку списков (в то время, как стиль Perl тяготеет к потоковой обработке). Это означает, что используя newLisp, обычно выгоднее не обрабатывать отчет строчка - за строчкой, а прочитать его весь, разделить на список, состоящий из его частей (строк, слов в строках) и затем обработать, используя всю мощь ЛИСПа.

Для примера, рассмотрим простую задачу: Предположим, в файле "report.txt" вы имеете отчет вида:

+----------------------------+
|  Пример текстового отчета  |
+----------------------------+
| uid     | balance | volume |
+----------------------------+
| user1   | 100000  | 1000   |
| user2   | 200000  | 1234   |
........

и хотите узнать сумму по графе volume:
(apply +
  (map (fn (x) (int (x 3) 10))
       (filter (fn (x) (and (> (length x) 3) (regex "^[0-9]+$" (x 3))))
               (map (fn (x) (parse x " *\\| *" 0))
                    (parse (read-file "report.txt") "\n")))))
Новые функции:
"read-file" - читает файл, как строку.
"parse" - делит строку на список строк по указанным разделителям; при указании дополнительных опций, может использовать регулярные выражения (PCRE).
"\n" - символ конца строки.
"regex" - ищет соответствие регулярному выражению, если не находит, возвращает nil.
"int" - преобразует строку в целое число с основанием, указанным вторым параметром.

Чтобы разобраться, как это работает, запишем то же, но более аккуратно:

(define (split-report-line str)
  (map (fn (x) (parse x " *\\| *" 0))
       str))

(define (line-has-data? lst)
  (and (> (length lst) 3) (regex "^[0-9]+$" (lst 3))))

(apply +
  (map (fn (x) (int (x 3) 10))
       (filter line-has-data?
               (map split-report-line
                    (parse (read-file "report.txt") "\n")))))
Теперь основной код в комментариях практически не нуждается. Вкратце:
читается файл отчета (целиком),
разбивается на список строк,
строки разделяются на элементы,
отфильтровываются строки, имеющие данные для анализа,
из каждой строки берется четвертый элемент (индекс - 3) и преобразуется в число,
получившийся в результате список чисел суммируется.

Две вспомогательные функции также очевидны:
первая разделяет строку регулярным выражением (и за одно удаляет все лишние пробелы),
вторая проверяет разделенную строку-список на наличие числового значения в четвертом (интересующем нас) элементе.

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


Здесь я позволю себе прервать повествование, посчитав, что написано достаточно, и можно поддаться желанию поглядеть, что из зтого получится...

В завершение - несколько слов о возможностях языка newLISP.

newLISP создан и активно развивается стараниями Луца Мюллера (Lutz Mueller). Это интерпретатор, написанный на чистом Си, с применением только стандартной библиотеки libc. Такая реализация сделала newLISP кросс-платформенным языком, одинаково работающим под многими юниксами, всеми версиями M$ Windows и под MacOS X.

Не смотря на свой миниатюрный размер - один бинарный файл размером менее 200к, в newLISP встроены такие возможности, как:

newLISP опубликован под лицензией GNU GPL и его исходные тексты и бинарные файлы доступны по адресу http://newlisp.org.
На этом же сайте вы получите доступ к великолепной документации (на английском), полезным ссылкам, подсказкам и дружелюбному (англоязычному) форуму.

Среди документации следует особо выделить чрезвычайно полезные "официальные" документы:
"newLISP Manual and Reference" - справочник по языку и
"newLISP Design Patterns" - описание приемов программирования задач из всех основных областей применения языка.

Сайт автора данного опуса располагается по адресу http://en.feautec.pp.ru. На нем можно найти несколько полезных библиотек и неофициальный репозитарий пакетов Debian (для дистрибутивов sarge и etch).


БЛАГОДАРНОСТИ

Посетителям сайта linux.org.ru - некоторые их комментарии были весьма интересны.

Alex-у с форума newLISP Fan Club за комментарии по стилистике.

ЛИЦЕНЗИЯ

Данный текст может свободно распространяться полностью либо частично в электронных сетях при условии сохранения информации об авторе и сайте оригинала - http://en.feautec.pp.ru.

Допускается любая модификация текста при выполнении условий:
- отражения факта модификации в строке с номером версии в начале документа,
- извещения автора по e-mail, указанному в Copyright документа,
- безусловного согласия на включение любых фрагментов произведенных изменений в оригинальную версию документа (с отражением авторства в разделе "Благодарности").

Любое тиражирование данного документа в виде жестких копий (на бумажных либо электронных носителях информации), за исключением дистрибутивов newLISP и дистрибутивов свободных операционных систем, допускается только с письменного согласия автора.


(C) 2006, Дмитрий Черняк losthost@narod.ru.