Эта статья адресована программистам на "традиционных" процедурных и объектно-ориентированных языках. Ее цель - показать некоторые удобные приемы программирования, обычные для функционального языка LISP, но редко используемые в других языках.
ЛИСП - это целое семейство диалектов, наиболее известным и признанным среди которых является Common LISP. Однако, основой для этой статьи послужил упрощенный диалект newLISP, интрепретатор которого работает в любой операционной системе и который можно использовать для "повседневных задач" на уровне языка Perl. Различия между newLISP и Common LISP в рамках излагаемого материала абсолютно не существенны.
О том, где взять дистрибутив newLISP написано в последней главе.
Итак... попробуем!
При записи на ЛИСПе, список ограничивается круглыми скобками, а его элементы
разделяются пробелами.
Какие данные может содержать список? - в ЛИСПе - практически любые! Элементами
списка могут быть константы, имена переменных и функций (в терминологии ЛИСПа -
символы) и, конечно, другие списки:
(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".
(1 b) -> (("second" 2) ("third" 3))
срез со второго элемента и до конца списка.
(0 2 b) -> (("first" 1) ("second" 2))
срез с первого элемента списка длиной 2 элемента.
(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") - изолированных пространств имен, реализующих
основные принципы ООП в форме, максимально приспособленной для быстрого
"скриптинга".
(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 трактуются как "истина" (в т.ч., 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 и т.д. и, при совпадении, будет вычислено соответствующие выражение.
Обратите внимание, что в этом примере в качестве результирующих выражений используются
строковые константы.Теперь, с помощью функции "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", она заменяет все выражения, стоящие на местах "тестовых констант" на результат их вычисления - т.е. на константы.
Текст данного макроса пока что немного сложен - он станет понятен после глав "Преобразование списков" и "Анонимные функции".
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" - аналоги функций "+" и "/", но работающие с числами с плавающей точкой.
(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)
Одним из наиболее замечательных встроенных экземпляров является функция "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))По результату, эти выражения эквивалентны. Разница лишь в краткости записи.
(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", если ее аргумент - не пустая строка и не
пустой список.Обратной по действию является функция "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" - список строк, второй (опциональный) - строка-разделитель.И, конечно, самой "чудесной" является рассмотренная в предыдущей главе функция "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" и увидеть, как он работает.
Не смотря на то, что в реальной практике такой стиль не всегда возможен и эффективен, стремление к нему позволяет сделать программу более прозрачной и сделать логические ошибки более дружественными.
В ЛИСПе, благодаря его свойствам мета-языка, все делается наоборот. Для решения задачи исследуется предметная область и на основе возможностей ЛИСПа создается новый язык. Затем на этом языке записывается решение требуемой задачи. Если "вдруг" выясняется, что первоначальный план претерпел существенные изменения, не беда - на уже готовом языке несложно записать и что-то новое. Этот способ называется разработкой "снизу-вверх".
Особо приятным следствием разработки "снизу-вверх" является возможность на каждом уровне решения задачи использовать наиболее подходящий язык, опять же, легко транслируемый в логику естественного языка.
Не лишайте себя возможности писать так:
(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)))
Комментарии излишни, не правда ли?Отступ для вложенных списков данных - один пробел (поскольку открывающие скобки на предыдущей строчке могут следовать одна за другой).
Если у функции много аргументов и первый находится на одной строке с именем функции, остальные аргументы удобно писать под первым "в столбик" (как в предидущем примере).
Для специальных функций, вроде "let" или "if", у которых первый параметр имеет особое значение, первый параметр обычно начинают на одной строке с именем функции, а остальные - на других строках со стандартным отступом в два пробела:
(if (= a b) (println "равенство") (println "неравенство"))
Для контроля баланса скобок лучше использовать текстовый редактор, обеспечивающий возможность автоматического поиска парных скобок - это умеют все современные "программистские" редакторы.
В отсутствие "правильного" редактора, скобки удобно закрывать "вслепую" - глазами считать отступы кода с самого внутреннего до уровня, который надо завершить, и каждый раз вслепую нажимать закрывающую скобку на клавиатуре.
Для тех же задач неплохо подходит и 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")))))
Новые функции:Чтобы разобраться, как это работает, запишем то же, но более аккуратно:
(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")))))
Теперь основной код в комментариях практически не нуждается. Вкратце:
Две вспомогательные функции также очевидны:
первая разделяет строку регулярным выражением (и за одно удаляет все лишние пробелы),
вторая проверяет разделенную строку-список на наличие числового значения в четвертом
(интересующем нас) элементе.
Быть может, на Perl это удастся записать немного компактнее. Однако не стоит забывать, что основной результат данного скрипта - получение списка всех элементов отчета, которые затем можно группировать, фильтровать, соотносить и пересчитывать с использованием всего богатства инструментов обработки списков.
Не смотря на свой миниатюрный размер - один бинарный файл размером менее 200к, в newLISP встроены такие возможности, как:
Среди документации следует особо выделить чрезвычайно полезные "официальные"
документы:
"newLISP Manual and Reference" - справочник по языку и
"newLISP Design Patterns"
- описание приемов программирования задач из всех
основных областей применения языка.
Сайт автора данного опуса располагается по адресу http://en.feautec.pp.ru. На нем можно найти несколько полезных библиотек и неофициальный репозитарий пакетов Debian (для дистрибутивов sarge и etch).
Alex-у с форума newLISP Fan Club за комментарии по стилистике.
Допускается любая модификация текста при выполнении условий:
- отражения факта модификации в строке с номером версии в начале документа,
- извещения автора по e-mail, указанному в Copyright документа,
- безусловного согласия на включение любых фрагментов произведенных изменений в оригинальную
версию документа (с отражением авторства в разделе "Благодарности").
Любое тиражирование данного документа в виде жестких копий (на бумажных либо электронных носителях информации), за исключением дистрибутивов newLISP и дистрибутивов свободных операционных систем, допускается только с письменного согласия автора.