Кто-то пытается продать мне Лисп как сверхмощный язык, который может делать все когда-либо, и еще кое-что.
Есть ли практический пример кода возможностей Лиспа?
(Желательно вместе с эквивалентной логикой, закодированной на обычном языке.)
Кто-то пытается продать мне Лисп как сверхмощный язык, который может делать все когда-либо, и еще кое-что.
Есть ли практический пример кода возможностей Лиспа?
(Желательно вместе с эквивалентной логикой, закодированной на обычном языке.)
Мне нравятся макросы.
Вот код, позволяющий убрать атрибуты людей из 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 -..., который связывает значения на основе выражения, из которого вы можете получить значения. Давайте представим наш собственный макрос, работающий так, 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))
Вы видели, как группа строк внезапно исчезла и была заменена одной единственной строкой? Как это сделать? Конечно, с помощью макросов - кода, который пишет код! Макросы в Лиспе - это совершенно другое животное, чем те, которые вы можете найти в C / C ++ с помощью препроцессора: здесь вы можете запустить настоящий код Lisp (а не #define
вздор в cpp) который генерирует код Lisp до того, как будет скомпилирован другой код. Макросы могут использовать любой реальный код Лиспа, то есть обычные функции. По сути, никаких ограничений.
Итак, посмотрим, как это было сделано. Чтобы заменить один атрибут, мы определяем функцию.
(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-выражения.
По сути, он позволяет вам писать свой код глаголами, которые можно найти в проблемной области, вместо низкоуровневых примитивов из языка! В качестве глупого примера рассмотрим следующее (если 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.
Надеюсь, что это проясняет ситуацию. Макросы - это мощный инструмент, и одна из функций Лиспа мне нравится.
Лучшим примером, о котором я могу думать, является широко доступная книга Пола Грэма On Lisp. Полный PDF-файл можно скачать по ссылке, которую я только что дал. Вы также можете попробовать Практический 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
Выполнение этого на любом языке, отличном от Lisp, оставлено в качестве упражнения для читателя ...
Собственно, хороший практический пример - это макрос LOOP в Лиспе.
http://www.ai.sri.com/pkarp/loop.html
Макрос LOOP - это просто макрос Лиспа. Тем не менее, он в основном определяет миниатюрный циклический DSL (язык, специфичный для домена).
Просматривая этот небольшой учебник, вы можете увидеть (даже новичку), что трудно понять, какая часть кода является частью макроса Loop, а какая - «нормальным» Лиспом.
И это один из ключевых компонентов выразительности Лиспа: новый код действительно невозможно отличить от системы.
В то время как, скажем, в Java вы не можете (с первого взгляда) узнать, какая часть программы поступает из стандартной библиотеки Java, а не из вашего собственного кода или даже из сторонней библиотеки, вы ДЕЙСТВИТЕЛЬНО знаете, какая часть кода - это язык Java, а не просто вызов методов для классов. Конечно, это ВСЕ «язык Java», но как программист вы ограничены только выражением своего приложения в виде комбинации классов и методов (а теперь и аннотаций). В то время как в Лиспе буквально все открыто.
Рассмотрим интерфейс Common SQL для подключения Common Lisp к SQL. Здесь http://clsql.b9.com/manual/loop-tuples.html, они показывают, как макрос CL Loop расширяется, чтобы сделать привязку SQL «гражданином первого класса».
Вы также можете наблюдать такие конструкции, как «[выберите [имя] [фамилия]: от [сотрудник]: упорядочить по [фамилия]]». Это часть пакета CL-SQL и реализована как «макрос чтения».
Видите ли, в Лиспе вы можете не только создавать макросы для создания новых конструкций, таких как структуры данных, управляющие структуры и т. Д., Но вы даже можете изменять синтаксис языка с помощью макроса чтения. Здесь они используют макрос чтения (в данном случае символ '[') для перехода в режим SQL, чтобы SQL работал как встроенный SQL, а не как просто необработанные строки, как во многих других языках.
Наша задача как разработчиков приложений - преобразовать наши процессы и конструкции в форму, понятную процессору. Это означает, что нам неизбежно приходится «говорить свысока» с компьютерным языком, поскольку он нас «не понимает».
Common Lisp - одна из немногих сред, в которых мы можем не только создавать наше приложение сверху вниз, но и поднимать язык и среду, чтобы идти навстречу. Мы можем кодировать с обеих сторон.
Каким бы элегантным это ни было, это не панацея. Очевидно, что есть и другие факторы, влияющие на выбор языка и среды. Но, безусловно, стоит изучить и поиграть. Я считаю, что изучение Лиспа - отличный способ продвинуть свое программирование даже на других языках.
Мне нравится Common Lisp Object System (CLOS) и мультиметоды.
Большинство, если не все, объектно-ориентированные языки программирования имеют базовые понятия о классах и методах. Следующий фрагмент в Python определяет классы PeelingTool и Vegetable (что-то похожее на Шаблон посетителя):
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, и Banana его принимает. Но он должен принадлежать к классу PeelingTool, поэтому его можно использовать, только если у вас есть экземпляр класса PeelingTool.
Версия объектной системы Common Lisp:
(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 и C ++
Автор статьи Брэндон Корфман пишет об исследовании, в котором сравнивает решения на Java, C ++ и Lisp с проблемой программирования, а затем пишет свое собственное решение на C ++. Тестовое решение - это 45 строк Лиспа Питера Норвига (написано за 2 часа).
Корфман считает, что его решение сложно сократить до менее чем 142 строк C ++ / STL. Его анализ того, почему, интересно читать.
Что мне больше всего нравится в системах Lisp (и Smalltalk), так это то, что они кажутся живыми. Вы можете легко исследовать и изменять системы Lisp во время их работы.
Если это звучит загадочно, запустите Emacs и введите какой-нибудь код на Лиспе. Введите C-M-x
и вуаля! Вы только что изменили Emacs из Emacs. Вы можете продолжить и переопределить все функции Emacs во время его работы.
Другое дело, что эквивалентность кода = списка делает границу между кодом и данными очень тонкой. А благодаря макросам очень легко расширить язык и быстро создать 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 Это привязка Common Lisp к Selenium (среда тестирования веб-браузера), но вместо отображения каждого метода он читает XML-документ определения API-интерфейса Selenium во время компиляции и генерирует код сопоставления с использованием макросов. Вы можете увидеть сгенерированный API здесь: common-lisp.net/project/cl-selenium/api/selenium-package/index.html
По сути, это запуск макросов с внешними данными, которые в данном случае являются XML-документом, но могут быть столь же сложными, как чтение из базы данных или сети. Это сила того, что вся среда Lisp доступна вам во время компиляции.
Узнайте, как расширить 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>"
И он хорошо сочетается с другими структурами Лиспа, такими как макросы и функции. теперь сравните это с JSP ...
Я был студентом ИИ в Массачусетском технологическом институте в 1970-х. Как и любой другой ученик, я считал, что язык превыше всего. Тем не менее Лисп был основным языком. Вот некоторые вещи, для которых я все еще считаю, что он очень хорош:
Символическая математика. Легко и поучительно написать символическое дифференцирование выражения и алгебраическое упрощение. Я до сих пор их делаю, хотя делаю их на Си-как угодно.
Доказательство теорем. Время от времени я впадаю в временную запой с ИИ, как будто пытаюсь доказать, что сортировка вставки правильная. Для этого мне нужно выполнять символические манипуляции, и я обычно прибегаю к Lisp.
Немного предметно-ориентированных языков. Я знаю, что Lisp не действительно практичен, но если я хочу немного попробовать, DSL без необходимости погружаться в парсинг и т. д., макросы Lisp упрощают эту задачу.
Маленькие игровые алгоритмы, такие как поиск по дереву минимаксной игры, могут быть выполнены в виде трех строк.
В основном то, что Lisp делает для меня, - это умственные упражнения. Затем я смогу перенести это на более практичные языки.
P.S. Говоря о лямбда-исчислении, то, что также началось в 1970-х, в том же AI milieu, заключалось в том, что объектно-ориентированный объект начал вторгаться в мозг каждого, и каким-то образом интерес к тому, что это есть, похоже, вытеснил большой интерес к тому, что это хорошо для. Т.е. работа над машинным обучением, естественным языком, зрением, решением проблем - все это ушло в дальний конец комнаты, в то время как классы, сообщения, типы, полиморфизм и т. д. пошли вперед.
Вы видели это объяснение того, почему макросы мощные и гибкие? Хотя нет примеров на других языках, извините, но это может помочь вам в макросах.
@Отметка,
Хотя в том, что вы говорите, есть доля правды, я считаю, что это не всегда так просто.
Программисты и люди в целом не всегда находят время, чтобы оценить все возможности и решить сменить язык. Часто решения принимают менеджеры или школы, в которых преподают первые языки ... и программистам никогда не нужно вкладывать достаточно времени, чтобы достичь определенного уровня, если они могут решить, что этот язык экономит мне больше времени, чем этот язык.
Кроме того, вы должны признать, что языки, поддерживаемые крупными коммерческими организациями, такими как Microsoft или Sun, всегда будут иметь преимущество на рынке по сравнению с языками без такой поддержки.
Чтобы ответить на исходный вопрос, Пол Грэм пытается привести пример здесь, хотя я признаю это не обязательно так практично, как хотелось бы :-)
Одна вещь, которая произвела на меня впечатление, - это возможность написать собственное расширение объектно-ориентированного программирования, если вам не нравится включенный CLOS.
Один из них находится в Garnet, а другой - в В Lisp.
Также существует пакет под названием Screamer, который позволяет недетерминированное программирование (чего я не оценено).
Любой язык, который позволяет вам изменять его для поддержки различных парадигм программирования, должен быть гибким.
Вы можете найти этот пост Эрика Норманда полезным. Он описывает, как по мере роста кодовой базы Lisp помогает, позволяя вам построить язык для вашего приложения. Хотя это часто требует дополнительных усилий на раннем этапе, позже это дает вам большое преимущество.
Тот простой факт, что это мультипарадигмальный язык, делает его очень гибким.
Джон Остерхаут сделал следующее интересное наблюдение относительно Лиспа в 1994 году:
Разработчики языков любят спорить о том, почему тот или иной язык должен быть лучше или хуже априори, но ни один из этих аргументов не имеет большого значения. В конечном итоге все языковые проблемы решаются, когда пользователи голосуют ногами.
Если [язык] делает людей более продуктивными, они будут им пользоваться; когда появится какой-нибудь другой язык, который лучше (или если он уже есть), тогда люди переключатся на этот язык. Это Закон, и это хорошо. Закон говорит мне, что Scheme (или любой другой диалект Lisp), вероятно, не является «правильным» языком: слишком много людей голосовали ногами за последние 30 лет.
http://www.vanderburg.org/OldPages/Tcl/war/0009.html