Практический пример гибкости Lisp?
Кто-то пытается продать мне Лисп, как сверхмощный язык, который может делать все что угодно, а потом и кое-что.
Есть ли практический пример кода силы Лиспа?
(Предпочтительно вместе с эквивалентной логикой, закодированной на обычном языке.)
18 ответов
Мне нравятся макросы.
Вот код, чтобы убрать атрибуты для людей из LDAP. Я просто случайно обнаружил этот код и настроил его, чтобы он был полезен для других.
Некоторые люди растеряны из-за предполагаемого штрафа во время выполнения макросов, поэтому в конце я добавил попытку прояснить ситуацию.
В начале было дублирование
(defun ldap-users ()
(let ((people (make-hash-table :test 'equal)))
(ldap:dosearch (ent (ldap:search *ldap* "(&(telephonenumber=*) (cn=*))"))
(let ((mail (car (ldap:attr-value ent 'mail)))
(uid (car (ldap:attr-value ent 'uid)))
(name (car (ldap:attr-value ent 'cn)))
(phonenumber (car (ldap:attr-value ent 'telephonenumber))))
(setf (gethash uid people)
(list mail name phonenumber))))
people))
Вы можете думать о "привязке let" как о локальной переменной, которая исчезает за пределами формы LET. Обратите внимание на форму привязок - они очень похожи, отличаются только атрибутом сущности LDAP и именем ("локальной переменной"), к которому привязывается значение. Полезно, но немного многословно и содержит дубликаты.
В поисках красоты
Теперь, разве не было бы хорошо, если бы у нас не было всего этого дублирования? Обычно используется макрос WITH-... macros, который связывает значения на основе выражения, из которого можно получить значения. Давайте представим наш собственный макрос, который работает так: WITH-LDAP-ATTRS, и заменим его в нашем исходном коде.
(defun ldap-users ()
(let ((people (make-hash-table :test 'equal))) ; equal so strings compare equal!
(ldap:dosearch (ent (ldap:search *ldap* "(&(telephonenumber=*) (cn=*))"))
(with-ldap-attrs (mail uid name phonenumber) ent
(setf (gethash uid people)
(list mail name phonenumber))))
people))
Вы видели, как группа строк внезапно исчезла и была заменена только одной строкой? Как это сделать? Конечно, используя макросы - код, который пишет код! Макросы в Lisp - это совершенно другое животное, чем те, которые вы можете найти в C/C++ с помощью препроцессора: здесь вы можете запустить настоящий код на Lisp (не #define
fluff in cpp), который генерирует код на Лиспе до компиляции другого кода. Макросы могут использовать любой настоящий лисповский код, т. Е. Обычные функции. По существу никаких ограничений.
Избавление от Гадкого
Итак, посмотрим, как это было сделано. Чтобы заменить один атрибут, мы определяем функцию.
(defun ldap-attr (entity attr)
`(,attr (car (ldap:attr-value ,entity ',attr))))
Синтаксис обратной цитаты выглядит немного странно, но то, что он делает, легко. Когда вы вызываете LDAP-ATTRS, он выдаст список, содержащий значение attr
(это запятая), а затем car
("первый элемент в списке" (на самом деле это пара пар), и на самом деле есть функция first
вы также можете использовать), который получает первое значение в списке, возвращаемом ldap:attr-value
, Поскольку это не тот код, который мы хотим запустить при компиляции кода (получение значений атрибутов - это то, что мы хотим делать при запуске программы), мы не добавляем запятую перед вызовом.
Тем не мение. Двигаясь дальше, к остальной части макроса.
(defmacro with-ldap-attrs (attrs ent &rest body)
`(let ,(loop for attr in attrs
collecting `,(ldap-attr ent attr))
,@body))
,@
-синтаксис - помещать содержимое списка куда-нибудь, вместо реального списка.
Результат
Вы можете легко убедиться, что это даст вам правильную вещь. Макросы часто пишутся таким образом: вы начинаете с кода, который вы хотите упростить (вывод), вместо того, чтобы писать вместо него (ввод), а затем начинаете формировать макрос до тех пор, пока ввод не даст правильный вывод. Функция macroexpand-1
скажет вам, если ваш макрос правильный:
(macroexpand-1 '(with-ldap-attrs (mail phonenumber) ent
(format t "~a with ~a" mail phonenumber)))
оценивает
(let ((mail (car (trivial-ldap:attr-value ent 'mail)))
(phonenumber (car (trivial-ldap:attr-value ent 'phonenumber))))
(format t "~a with ~a" mail phonenumber))
Если вы сравните LET-привязки расширенного макроса с кодом в начале, вы обнаружите, что он в той же форме!
Время компиляции и время выполнения: макросы и функции
Макрос - это код, который запускается во время компиляции, с добавленным поворотом, что они могут вызывать любую обычную функцию или макрос, как им угодно! Это не что иное, как причудливый фильтр, который принимает некоторые аргументы, применяет некоторые преобразования, а затем передает компилятору полученные s-exps.
По сути, он позволяет писать код в глаголах, которые можно найти в проблемной области, вместо низкоуровневых примитивов из языка! В качестве глупого примера рассмотрим следующее (если when
не был уже встроенным)::
(defmacro my-when (test &rest body)
`(if ,test
(progn ,@body)))
if
является встроенным примитивом, который позволяет вам выполнять только одну форму в ветвях, и если вы хотите иметь более одной, ну, вам нужно использовать progn
::
;; one form
(if (numberp 1)
(print "yay, a number"))
;; two forms
(if (numberp 1)
(progn
(assert-world-is-sane t)
(print "phew!"))))
С нашим новым другом, my-when
, мы могли бы а) использовать более подходящий глагол, если у нас нет ложной ветви, и б) добавить неявный оператор секвенирования, т.е. progn
::
(my-when (numberp 1)
(assert-world-is-sane t)
(print "phew!"))
Скомпилированный код никогда не будет содержать my-when
тем не менее, поскольку на первом проходе все макросы раскрываются, поэтому штраф за время выполнения не применяется!
Lisp> (macroexpand-1 '(my-when (numberp 1)
(print "yay!")))
(if (numberp 1)
(progn (print "yay!")))
Обратите внимание, что macroexpand-1
делает только один уровень расширений; возможно (скорее всего, на самом деле!), что расширение продолжается и дальше вниз. Однако, в конце концов, вы попадете в детали реализации, специфичные для компилятора, которые часто не очень интересны. Но продолжая расширять результат, вы в конечном итоге либо получите больше деталей, либо просто вернете введенный s-exp.
Надеюсь, что все проясняет. Макросы - это мощный инструмент, и мне нравится одна из функций в Лиспе.
Лучший пример, который я могу придумать, широко доступен - это книга Пола Грэма " О Лиспе". Полный PDF можно скачать по ссылке, которую я только что дал. Вы также можете попробовать Practical Common Lisp (также полностью доступный в Интернете).
У меня много непрактичных примеров. Однажды я написал программу из примерно 40 строк lisp, которая могла бы анализировать себя, обрабатывать ее источник как список lisp, выполнять обход дерева по списку и создавать выражение, которое вычисляется как WALDO, если в источнике существует идентификатор waldo, или вычисляется как ноль, если бы Уолдо не было. Возвращаемое выражение было создано путем добавления вызовов car/cdr к исходному источнику, который был проанализирован. Я понятия не имею, как сделать это на других языках в 40 строк кода. Возможно, Perl может сделать это в еще меньшем количестве строк.
Вы можете найти эту статью полезной: http://www.defmacro.org/ramblings/lisp.html
Тем не менее, очень и очень трудно привести короткие практические примеры возможностей Lisp, потому что они действительно проявляются только в нетривиальном коде. Когда ваш проект достигнет определенного размера, вы по достоинству оцените возможности абстракции Lisp и будете рады, что вы использовали их. С другой стороны, достаточно короткие примеры кода никогда не дадут вам удовлетворительной демонстрации того, что делает Lisp великолепным, потому что предопределенные сокращения других языков будут выглядеть привлекательнее в небольших примерах, чем гибкость Lisp в управлении абстракциями, специфичными для предметной области.
В Лиспе есть множество потрясающих функций, но мне особенно нравятся макросы, потому что на самом деле больше нет барьера между тем, что определяет язык, и тем, что я определяю. Например, Common Lisp не имеет конструкции while. Однажды я реализовал это в своей голове во время ходьбы. Это просто и чисто:
(defmacro while (condition &body body)
`(if ,condition
(progn
,@body
(do nil ((not ,condition))
,@body))))
И вуаля! Вы только что расширили язык Common Lisp новой фундаментальной конструкцией. Теперь вы можете сделать:
(let ((foo 5))
(while (not (zerop (decf foo)))
(format t "still not zero: ~a~%" foo)))
Который напечатал бы:
still not zero: 4
still not zero: 3
still not zero: 2
still not zero: 1
Делать это на любом не-лиспском языке оставляют читателю в качестве упражнения...
Мне нравится Common Lisp Object System (CLOS) и мультиметоды.
Большинство, если не все, объектно-ориентированные языки программирования имеют базовые понятия о классах и методах. Следующий фрагмент в Python определяет классы PeelingTool и Vegetable (что-то похожее на шаблон Visitor):
class PeelingTool:
"""I'm used to peel things. Mostly fruit, but anything peelable goes."""
def peel(self, veggie):
veggie.get_peeled(self)
class Veggie:
"""I'm a defenseless Veggie. I obey the get_peeled protocol
used by the PeelingTool"""
def get_peeled(self, tool):
pass
class FingerTool(PeelingTool):
...
class KnifeTool(PeelingTool):
...
class Banana(Veggie):
def get_peeled(self, tool):
if type(tool) == FingerTool:
self.hold_and_peel(tool)
elif type(tool) == KnifeTool:
self.cut_in_half(tool)
Вы положили peel
метод в PeelingTool и пусть Банан его примет. Но он должен принадлежать классу PeelingTool, поэтому его можно использовать, только если у вас есть экземпляр класса PeelingTool.
Версия Common Lisp Object System:
(defclass peeling-tool () ())
(defclass knife-tool (peeling-tool) ())
(defclass finger-tool (peeling-tool) ())
(defclass veggie () ())
(defclass banana (veggie) ())
(defgeneric peel (veggie tool)
(:documentation "I peel veggies, or actually anything that wants to be peeled"))
;; It might be possible to peel any object using any tool,
;; but I have no idea how. Left as an exercise for the reader
(defmethod peel (veggie tool)
...)
;; Bananas are easy to peel with our fingers!
(defmethod peel ((veggie banana) (tool finger-tool))
(with-hands (left-hand right-hand) *me*
(hold-object left-hand banana)
(peel-with-fingers right-hand tool banana)))
;; Slightly different using a knife
(defmethod peel ((veggie banana) (tool knife-tool))
(with-hands (left-hand right-hand) *me*
(hold-object left-hand banana)
(cut-in-half tool banana)))
Все может быть написано на любом языке, который завершен по Тьюрингу; разница между языками заключается в том, сколько обручей вам нужно пройти, чтобы получить эквивалентный результат.
Мощные языки, такие как Common Lisp, с такими функциями, как макросы и CLOS, позволяют быстро и легко достигать результатов, не перепрыгивая через столько обручей, что вы либо соглашаетесь на решение на низком уровне, либо становитесь кенгуру.
На самом деле хорошим практическим примером является макрос LISP LOOP.
http://www.ai.sri.com/pkarp/loop.html
Макрос LOOP - это просто макрос Lisp. Тем не менее, он в основном определяет мини-циклический DSL (предметно-ориентированный язык).
Просматривая этот небольшой учебник, вы можете увидеть (даже новичком), что трудно понять, какая часть кода является частью макроса Loop, а какая - "нормальный" Лисп.
И это один из ключевых компонентов выразительности Lisps, который новый код действительно нельзя отличить от системы.
Хотя, скажем, в Java вы, возможно, не сможете (на первый взгляд) узнать, какая часть программы поступает из стандартной библиотеки Java по сравнению с вашим собственным кодом или даже сторонней библиотекой, вы действительно знаете, какая часть кода это язык Java, а не просто вызовы методов для классов. Конечно, это ВСЕ "язык Java", но, как программист, вы ограничены только тем, чтобы выражать свое приложение как комбинацию классов и методов (а теперь и аннотаций). Принимая во внимание, что в Лиспе буквально все готово.
Рассмотрим интерфейс Common SQL для подключения Common Lisp к SQL. Здесь, http://clsql.b9.com/manual/loop-tuples.html, они показывают, как макрос Loop CL расширяется, чтобы сделать привязку SQL "гражданином первого класса".
Вы также можете наблюдать такие конструкции, как "[выберите [имя] [фамилия]: от [сотрудник]: упорядочить по [фамилия]]". Это часть пакета CL-SQL и реализована как "макрос чтения".
Видите ли, в Лиспе вы можете не только создавать макросы для создания новых конструкций, таких как структуры данных, структуры управления и т. Д. Но вы даже можете изменить синтаксис языка с помощью макроса читателя. Здесь они используют макрос для чтения (в данном случае символ "[") для перехода в режим SQL, чтобы заставить SQL работать как встроенный SQL, а не как просто необработанные строки, как во многих других языках.
Как разработчики приложений, наша задача - преобразовать наши процессы и конструкции в форму, понятную процессору. Это означает, что мы неизбежно должны "разговаривать" с языком компьютера, поскольку он "не понимает" нас.
Common Lisp - это одна из немногих сред, где мы можем не только создавать наше приложение сверху вниз, но и поднимать язык и среду, чтобы идти навстречу. Мы можем кодировать на обоих концах.
Ум, как бы элегантно это ни было, это не панацея. Очевидно, есть и другие факторы, которые влияют на выбор языка и среды. Но, безусловно, стоит учиться и играть. Я думаю, что изучение Lisp - отличный способ продвинуть ваше программирование, даже на других языках.
Я нашел эту статью довольно интересной:
Сравнение языков программирования: Lisp vs C++
Автор статьи, Брэндон Корфман, пишет об исследовании, в котором сравниваются решения на Java, C++ и Lisp с проблемой программирования, а затем пишет свое собственное решение на C++. Эталонное решение - 45 строк Lisp Питера Норвига (написано за 2 часа).
Корфман считает, что трудно сократить его решение до менее чем 142 строк C++/STL. Его анализ почему, интересно читать.
Что мне больше всего нравится в системах Lisp (и Smalltalk), так это то, что они чувствуют себя живыми. Вы можете легко исследовать и модифицировать системы Lisp, пока они работают.
Если это звучит загадочно, запустите Emacs и введите код на Лиспе. Тип C-M-x
и вуаля! Вы только что изменили Emacs из Emacs. Вы можете продолжить и переопределить все функции Emacs во время его работы.
Другое дело, что эквивалентность code = list делает границу между кодом и данными очень тонкой. А благодаря макросам очень легко расширить язык и создать быстрые DSL.
Например, можно закодировать базовый компоновщик HTML, с помощью которого код очень близок к полученному выводу HTML:
(html
(head
(title "The Title"))
(body
(h1 "The Headline" :class "headline")
(p "Some text here" :id "content")))
=>
<html>
<head>
<title>The title</title>
</head>
<body>
<h1 class="headline">The Headline</h1>
<p id="contents">Some text here</p>
</body>
</html>
В коде Lisp автоматическое отступление делает код похожим на вывод, за исключением того, что нет никаких закрывающих тегов.
Мне нравится этот пример макроса с http://common-lisp.net/cgi-bin/viewcvs.cgi/cl-selenium/?root=cl-selenium Это обычная привязка Lisp к Selenium (тестовая среда веб-браузера), но вместо сопоставления каждого метода, он читает собственный XML-документ определения API Selenium во время компиляции и генерирует код сопоставления, используя макросы. Вы можете увидеть сгенерированный API здесь: common-lisp.net/project/cl-selenium/api/selenium-package/index.html
По сути, это приводит в действие макросы с внешними данными, которые в данном случае являются XML-документами, но могут быть такими же сложными, как чтение из базы данных или сети. Это сила доступности всей среды Lisp во время компиляции.
Я был студентом AI в MIT в 1970-х. Как и любой другой студент, я думал, что язык имеет первостепенное значение. Тем не менее, Лисп был основным языком. Вот некоторые вещи, которые я все еще думаю, что это довольно хорошо для:
Символическая математика. Легко и поучительно написать символическое дифференцирование выражения и алгебраическое упрощение. Я все еще делаю это, хотя я делаю их в C-что угодно.
Доказательство теорем. Время от времени я делаю временный переворот ИИ, как попытка доказать, что сортировка вставок правильна. Для этого мне нужно сделать символические манипуляции, и я обычно прибегаю к Лиспу.
Маленькие предметно-ориентированные языки. Я знаю, что Lisp на самом деле не практичен, но если я хочу опробовать небольшой DSL без необходимости разбираться с синтаксическим анализом и т. Д., Макросы Lisp облегчают это.
Небольшие игровые алгоритмы, такие как поиск минимаксного игрового дерева, могут быть выполнены в три строки.
- Хотите попробовать лямбда-исчисление? Это легко в Лиспе.
Главным образом то, что делает Лисп для меня, это умственные упражнения. Тогда я могу перенести это на более практичные языки.
PS Говоря о лямбда-исчислении, то, что также началось в 1970-х годах в той же самой AI-среде, заключалось в том, что ОО начал вторгаться в мозг каждого, и каким-то образом интерес к тому, что кажется, вытеснил большой интерес к тому, для чего он хорош. Т.е. работа над машинным обучением, естественным языком, зрением, решением проблем - все это уходило в конец комнаты, а уроки, сообщения, типы, полиморфизм и т. Д. Шли вперед.
Одна вещь, которая мне нравится, это то, что я могу обновлять код "во время выполнения" без потери состояния приложения. Это полезно только в некоторых случаях, но когда это полезно, иметь его уже там (или, при минимальных затратах во время разработки) НАМНОГО дешевле, чем реализовывать его с нуля. Тем более, что это происходит по принципу "почти нет".
Посмотрите, как вы можете расширить Common Lisp с помощью шаблонов XML: пример XML cl-quasi-quote, страница проекта,
(babel:octets-to-string
(with-output-to-sequence (*html-stream*)
<div (constantAttribute 42
someJavaScript `js-inline(print (+ 40 2))
runtimeAttribute ,(concatenate 'string "&foo" "&bar"))
<someRandomElement
<someOther>>>))
=>
"<div constantAttribute=\"42\"
someJavaScript=\"javascript: print((40 + 2))\"
runtimeAttribute=\"&foo&bar\">
<someRandomElement>
<someOther/>
</someRandomElement>
</div>"
По сути, это то же самое, что и обратный читатель Lisp (который предназначен для кавычек в списках), но он также работает для различных других вещей, таких как XML (установлен по специальному синтаксису <>), JavaScript (установлен по `js-inline) и т. Д.,
Чтобы было понятно, это реализовано в пользовательской библиотеке! И он компилирует статические части XML, JavaScript и т. Д. В буквенные байтовые массивы в кодировке UTF-8, которые готовы для записи в сетевой поток. С простым ,
(запятая) вы можете вернуться к lisp и чередовать сгенерированные во время выполнения данные в буквенные байтовые массивы.
Это не для слабонервных, но это то, что библиотека компилирует выше:
(progn
(write-sequence
#(60 100 105 118 32 99 111 110 115 116 97 110 116 65 116 116 114 105 98
117 116 101 61 34 52 50 34 32 115 111 109 101 74 97 118 97 83 99 114
105 112 116 61 34 106 97 118 97 115 99 114 105 112 116 58 32 112 114
105 110 116 40 40 52 48 32 43 32 50 41 41 34 32 114 117 110 116 105
109 101 65 116 116 114 105 98 117 116 101 61 34)
*html-stream*)
(write-quasi-quoted-binary
(let ((*transformation*
#<quasi-quoted-string-to-quasi-quoted-binary {1006321441}>))
(transform-quasi-quoted-string-to-quasi-quoted-binary
(let ((*transformation*
#<quasi-quoted-xml-to-quasi-quoted-string {1006326E51}>))
(locally
(declare (sb-ext:muffle-conditions sb-ext:compiler-note))
(let ((it (concatenate 'string "runtime calculated: " "&foo" "&bar")))
(if it
(transform-quasi-quoted-xml-to-quasi-quoted-string/attribute-value it)
nil))))))
*html-stream*)
(write-sequence
#(34 62 10 32 32 60 115 111 109 101 82 97 110 100 111 109 69 108 101 109
101 110 116 62 10 32 32 32 32 60 115 111 109 101 79 116 104 101 114 47
62 10 32 32 60 47 115 111 109 101 82 97 110 100 111 109 69 108 101 109
101 110 116 62 10 60 47 100 105 118 62 10)
*html-stream*)
+void+)
Для справки, два больших байтовых вектора в приведенном выше примере выглядят следующим образом при преобразовании в строку:
"<div constantAttribute=\"42\"
someJavaScript=\"javascript: print((40 + 2))\"
runtimeAttribute=\""
И второй:
"\">
<someRandomElement>
<someOther/>
</someRandomElement>
</div>"
И он хорошо сочетается с другими структурами Lisp, такими как макросы и функции. Теперь сравните это с JSP...
Вы взглянули на это объяснение того, почему макросы являются мощными и гибкими? Нет примеров на других языках, извините, но это может продать вас на макросах.
Одна особенность, которая произвела на меня впечатление, - это возможность написать собственное расширение объектно-ориентированного программирования, если вам не нравится включенный CLOS.
Один из них в " Гранате", а другой в " На Лиспе" Пола Грэма.
Есть также пакет Screamer, который позволяет недетерминированное программирование (которое я не оценивал).
Любой язык, который позволяет изменить его для поддержки различных парадигм программирования, должен быть гибким.
@Отметка,
Хотя в том, что вы говорите, есть доля правды, я считаю, что это не всегда так просто.
Программисты и люди в целом не всегда находят время, чтобы оценить все возможности и принять решение о переключении языков. Часто это решают менеджеры или школы, которые преподают первые языки... и программистам никогда не нужно тратить достаточно времени, чтобы достичь определенного уровня, если они могут решить, что этот язык экономит мне больше времени, чем этот язык.
Кроме того, вы должны признать, что языки, которые поддерживают огромные коммерческие структуры, такие как Microsoft или Sun, всегда будут иметь преимущество на рынке по сравнению с языками без такой поддержки.
Чтобы ответить на первоначальный вопрос, Пол Грэм пытается привести здесь пример, хотя я признаю, что он не обязательно настолько практичен, как хотелось бы:-)
Вы можете найти этот пост Эриком Нормандом полезным. Он описывает, как по мере роста кодовой базы Lisp помогает, позволяя вам создать язык для вашего приложения. Хотя это часто требует дополнительных усилий на раннем этапе, это дает вам большое преимущество позже.
Простой факт, что это мультипарадигмальный язык, делает его очень очень гибким.
Джон Ousterhout сделал это интересное наблюдение в отношении Lisp в 1994 году:
Разработчики языка любят спорить о том, почему этот язык или этот язык должны быть лучше или хуже априори, но ни один из этих аргументов не имеет большого значения. В конечном итоге все языковые проблемы решаются, когда пользователи голосуют ногами.
Если [язык] делает людей более продуктивными, они будут его использовать; когда придет какой-то другой язык, который лучше (или если он уже здесь), тогда люди переключатся на этот язык. Это Закон, и это хорошо. Закон говорит мне, что Scheme (или любой другой диалект Lisp), вероятно, не "правильный" язык: слишком много людей проголосовали ногами за последние 30 лет.