Как стать автором
Обновить

Комментарии 120

«Ибо воистину. Первый Язык, жемчужина посреди простых камней, и нет языков кроме Него. Скобки, в которых пустота — тело Его, мистическое двуединство кода и данных — дух Его, божественная рекурсия — сердце Его. Истинно говорю вам, избегающий света Его есть безумец, вот, свершается кара над главой его, и убогостью отмечены поделия его, подобные пустым глиняным горшкам рядом с хрустальным сосудом благодати Его. Принявший же и постигший истинный свет Его подобен прямой и отточенной стреле, чисты помыслы его и крепка рука его, и благословенны творения его, дарующие радость и утоляющие печали, ибо одухотворены духом Его и отмечены благодатью Его.»
Lisp — замечательный язык, но уж больно он наворочен. Всегда scheme больше нравился.
Схема слишком уж упрощена. Это замечательно если надо делать компилятор схемы, но не так привлекательно когда надо писать на ней. Слишком много приходится переизобретать.
Тем ни менее схема умеет оптимизировать хвостовую рекурсию, CL, на насколько я знаю — нет.
У вас несколько неверные сведения. CL не требует этой оптимизации, но все известные мне реализации её проводят. Исключение разве что ABCL (на JVM).
Реализации схемы на JVM тоже испытывают с этим трудности.
Но даже если и попадётся реализация этото не умеющая, то с помощью макросов можно всё сделать.
Common Lisp, а не просто Lisp. Есть самый первый LISP, который сделал Маккарти, есть семейство языков Lisp, и есть конкретные современные и не очень реализации, в том числе Common Lisp и Scheme.
Я так понимаю, что раз вы перевели, вас это интересует. Можно глупый вопрос? Когда кто-то пишет программу на лиспе, скобки биндит на отдельные клавиши? Не напрягает столько раз шифт зажимать?
Не слышал, чтобы кто-то специальным образом биндил клавиши для лиспа. Быстро привыкаешь к такому синтаксису и перестаёшь замечать какие-либо неудобства. К тому же, при нужном подходе код на лиспе получается очень кратким и руки устают куда меньше, чем с некоторыми другими языками.
Как-то сравнивал количество скобочек в коде на CL и в аналогичном коде на python. Выходило, что в коде на питоне их было не меньше. Разве что они были разные () [] {}.
Можно подумать в C подобных языках скобок будет сильно меньше.
Не такая уж большая разница, как писать, так — boo() или так — (foo).
Вот-вот. И даже смайлики бывают foo(bar(baz(x))). Ну а JavaScript вообще вне конкуренции со своими }); }); }); });
Честно говоря, это распространенное заблуждение. Например, после такого количества скобок
(function($){$(document).ready(function(){
});}());


лисп кажется раем
Честно говоря, вы мне открыли глаза. Елки, и не лень мне это писать? Действительно, сам не замечаешь, как привыкнешь.

Нужно настроить бинды для скобок…
Классный обзор. Некоторые моменты можно было бы осветить подробнее.

Например:

Compiler-macro'сы используются тем же самым format, который превращается в простые комманды вывода, условия и циклы. Они же очень эффективны в случае регулярных выражений, что позволяет cl-ppcre работать в среднем в два раза быстрее чем perl/pcre.

Кастомные reader-macro'сы типа uri-template.

Рестарты вообще достойны отдельной статьи.
Да, безусловно, многие вопросы стоят того, чтобы рассмотреть их подробнее, но статья и так получилась довольно длинная. Так что автор, на мой взгляд, поступил правильно, что описал всё в сжатом виде.

Интересующиеся могут обратиться за подробностями, например, к Practical Common Lisp, благо есть перевод, или к какой-нибудь ещё книге.
Я считаю, лисп нужно начинать учить с этой видяшки landoflisp.com
я считаю что с неё начинать не надо. имею таковую бумаге и считаю абсолютно размазанной и слабой. Если хотите познать лисп — читайте «On Lisp» Пола Грэма, а встречая непонятные места прибегайте к его же «ANSI Common Lisp».
вы не поняли, я имел в виду видео для привлечения внимания, а не одноименную книгу :)

А так да, On Lisp хорош.
Спасибо, неистово плюсанул вам. Давно хотел вот так обзорно почитать о лиспе. А можно поподробнее узнать о сферах его применения сейчас? Судя по графикам, он еще достаточно популярный язык.
Из известных проектов, использующих лисп, с ходу могу вспомнить AutoCAD и Maxima. Если говорить про все диалекты лиспа, то в последнее время особенно активно развивается Clojure. Много появляется новых проектов на нём, например Storm от твиттера или Datomic.

Вообще, область применения у лиспа очень широкая, можно найти проекты самого разного рода. Если интересно, списки успешных проектов есть на сайтах у LispWorks, Franc inc. и того же Clojure.
(я сегодня за зануду)
AutoCAD не использует CL, у него свой диалект — AutoLisp, особенностью коего является отсутствие макросов.
Всякие нестандартные применения можно посмотреть на сайте lispjobs.wordpress.com/
А есть какие-нибудь фреймворки для вэба на CL?
Ну вроде того же rails
Здесь www.cliki.net/Web есть список известного. Из тех, что на слуху: UnCommon Web, Weblocks, RESTAS.

Я сейчас пробую делать своё веб-приложение без фреймворков, напрямую используя hunchentoot и не испытываю никаких проблем.
Создатель Noir'а поразил меня тем, что не использует Post-Redirect-Get :)
 (resp/redirect "/success") 

В чём проблема-то?
В пропаганде неудобного для пользователей решения среди новичков.
Для пользователей — это для посетителей сайта или для программистов, использующих фреймворк? Посетители получают всё тот же Post-Redirect-Get, чем это неудобно программистам — тоже непонятно. Вроде проще некуда.
Я говорю о вот этом примере без редиректа из официальной документации:

(defpage [:post "/user/add"] {:as user}
(if (valid? user)
(layout
[:p "User added!"])
(render "/user/add" user)))

ИМХО, если по каким-то причинам автор не желает писать там редирект, то как минимум стоило бы предупредить, что в финальном продукте стоит его сделать. Собственно, такое письмо я и отправил в рассылку, получил молчание в ответ.
Restas — очень хороший framework, автор — archimag.
Лисп великий язык, но правду говорят, что начав писать на лиспе, очень сложно перестать писать на лиспе.
Язык очень большой и глубокий, пишу чуть больше года (на работе) и почти каждый день открываю новые и новые возможности. Но это кроет и минус — время обучения нового программиста достаточно велико.
P.S. Недавно читал документ по лиспу, датированный 1993 годом. Почти 20 лет прошло, а все что написано актуально и корректно и сейчас. Какой еще язык может похвастаться тем же?
Язык хороший, но является вещью в себе.

Кроме того, стандарт несколько устарел за более чем 10 лет. Например, в CL нет нормальных хеш-таблиц для объектов. Да, есть hash-table и функции вроде make-hash-table, но как туда задать собственную функцию проверки равенства?

Ну и банально — стандартные функции проверки равенства не расширяются на объекты. Надо городить свой огород например создавая generic-функцию object-equalp
Увы, такие проблемы действительно есть. Грустно это признавать, но сообщество у лиспа сейчас сравнительно небольшое и не очень активное, оттого и стандарт не обновляется и с библиотеками бывают проблемы.

Тем не менее, язык явно не стоит закапывать, надо развивать по мере сил и надеяться на лучшее :)
Не так уж и часто нужно что-то кроме строк/чисел/символов в качестве ключей. А если и нужно, то можно взять альтернативную реализацию хэш-таблиц (например genhash).

С равенствами такая беда потому, что CLOS разрабатывался как надстройка над базовым языком. Тот же самый setf учли, а равенство забыли :(

А если уж придираться к стандарту разрабатывавшемуся в 80-ых, то можно вспомнить, что в стандарт не вошли ли MOP ни Grey Streams. Также в стандарте ни слова о многопоточности (к слову в C++ она появилась только в 2011).

Но кроме стандарта начала 90-ых есть и куча библиотек нивелирующих эти недостатки: CloserMOP, bourdeau-threads, и т. п.
В C++ изначально идеология у Страуструпа была — если можно сделать библиотекой, то не зачем это в стандарт пихать. Библиотеки для многопоточности появились сразу же с C++, и не раз, ещё в 80х, его просили включить в стандарт, поддержку многопоточности.
Не знаю что сдохло в лесу к 2011ому, что он согласился :)
Стандарт C++ регламентирует не только сам язык, но и стандартную библиотеку. Они неотделимы.
Но volatile ка-то всё-таки в язык попал.

Многие другие языки вообще неотделимы от библиотек. Та же Java с java.lang.*.

Собственно, CL тоже можно свести к небольшому подмножеству, а остальное реализовать как библиотеку. На самом деле большинство реализаций так и делают.
Потому что Threads Cannot be Implemented as a Library (Hans Boehm). Многие аспекты семантики должны быть уточнены для работы в многопоточной среде, и, соответственно, компилятор должен учитывать при оптимизиции, как эффекты должны быть упорядочены с точки зрения других потоков.
Я прекрасно понимаю причины, по которым эти проблемы существуют.

Я б не стал сравнивать C++ и Common Lisp.

Вообще, ему бы значительно лучше чувствовалось, если бы он работал на lisp-машинах.

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

Сейчас, на мой взгляд, уже проще освоить clojure и писать под jvm, чем изучать common lisp.
Код на clojure хоть из java вызвать можно, а common lisp с этим некоторые проблемы.
Лисп-машины — утопическое прошлое. Они не способны конкурировать с универсальными машинами. Собственно говоря, они потому и исчезли. И даже их разработчики это понимали, продавая не сами машины, а их программные версии (Symbolics).

Актуального стандарта нету, это печально. Но он бы отличался от текущего незначительно. А разработка стандарта — забава не из дешёвых.

Хех. Clojure так же привязана к одной платформе как и лисп-машины в 80-ых. Да, эта платформа не аппаратная, но проблемы может вызвать не хуже. Например тяжба гугла с ораклом может повредить переносимости на андроид. Так же есть проблемы технического характера: реализовать мультиметоды эффективно не получается, вместо них приходится идти на компромисс — протоколы.

Если что, для JVM есть реализация CL — Armed Bear Common Lisp.
> Они не способны конкурировать с универсальными машинами.

Универсальные — это какие? И чем лисп-машина не универсальна?

> Clojure так же привязана к одной платформе как и лисп-машины в 80-ых. [...] Например тяжба гугла с ораклом может повредить переносимости на андроид.

ClojureCLR
Рич изначальное разрабатывал язык для 2-х платформ, но в итоге на это стало уходить слишком много времени, поэтому он сам сконцентрировался на JVM, оставив CLR другим энтузиастам.

> реализовать мультиметоды эффективно не получается, вместо них приходится идти на компромисс — протоколы.

Тут не совсем правильно говорить про компромиссы — у мультиметодов и протоколов всё-таки больше отличий, чем общего.
Универсальные это такие, на которых кроме лиспа можно запустить что-нибудь ещё.

Я думаю, кроме CLR реализации можно найти и другие такие же заброшенные. Clojure это всё-таки язык одной платформы (JVM). ClojureCLR вынуждена повторять всякие особенности JVM и недоразумения вроде recur, не смотря на то, что в CLR прекрасно поддерживается TCO. Думаю, что Clojure сейчас в той стадии в которой Lisp был до того как стал Common.

Мультиметоды куда более мощная концепция чем протоколы. Но обе решают задачу расширения функциональности закрытых (для модификации) классов.
> Универсальные это такие, на которых кроме лиспа можно запустить что-нибудь ещё.

Лисп-машина тьюринг полна, поэтому на ней можно запустить что угодно. То, что для неё не появилось кучи средств разработки под другие языки, было вызвано её низкой популярностью (а не наоборот, мол, низкая популярность была вызвана тем, что под неё нельзя создать средства разработки под другие языки).

> Я думаю, кроме CLR реализации можно найти и другие такие же заброшенные.

А с чего вы взяли, что ClojureCLR заброшен? То, что Рич сконцентрировался на JVM не значит, что версия для CLR заглохла — ею просто занимаются другие люди.

> ClojureCLR вынуждена повторять всякие особенности JVM и недоразумения вроде recur, не смотря на то, что в CLR прекрасно поддерживается TCO.

Изначально да, recur был сделан как костыль. Но постепенно народ просёк, что recur, в отличие от обычной TCO, сработает гарантированно, либо выкинет ошибку во время компиляции. Кроме того, Clojure-овский loop по определению нельзя рекурсивно вызвать никак, кроме как через recur.

> Думаю, что Clojure сейчас в той стадии в которой Lisp был до того как стал Common.

Эээ. Common Lisp стал результатом работы комиссии по универсализации всего огромного набора лиспов, существовавших на тот момент. Поэтому и получился такой гигант, вмещающий в себя всё. Clojure изначально пошёл по противоположной дороге — простоты и концептуальной целостности, и Рич неоднократно подчёркивал желание побороть те недостатки, которые получил CL в результате унификации.

Чтобы расширить функциональность, достаточно обычных функций. То, что объединяет мультиметоды и протоколы — это диспетчеризация вызовов по типу объекта. Чтобы `(str person)` показывало `Person(Иванов Иван Иванович)`, а `(str car)` показывало `Car(Mercedes Benz)`. Но если целью мультиметодов всегда была именно диспетчеризация (и с этой целью они справляются отлично, позволяя распределять методы по произвольному признаку и любому количеству аргументов), то целью протоколов была замена (и частично исправление) Java-овских интерфейсов со всеми вытекающими — эффективная диспетчеризация по типу, совместимость с Java-овским кодом, введение структурированности и иерархия типов.
>… постепенно народ просёк…
Свыкся.
Я не могу понять, чем вам recur не угодил? Когда вы пишете рекурсивный код, вы же всё равно расчитываете на то, что он будет оптимизирован и превратится, например, в цикл? Так какая разница, написать ещё раз имя функции или просто recur?
Recur не позволяет писать взаимно-рекурсивные функции. Ну и эстетически тоже не привлекает, от while не далеко ушёл.
Для взаимно рекурсивных функций существует trampoline, который гарантированно сделает то, что я его попрошу, а не оставит это на откуп оптимизациям компилятора.
Что же касается эстетики — то это вопрос привыкания. Большинство людей при первом знакомстве с любым лиспом испытывают жестокое чувство неприятия, но после некоторого времени изучения начинают видеть его красоту и наслаждаться тем, что прежде не переваривали.
trampoline тоже не всегда спасает. Вместо переполнения стека он будет забивать кучу промежуточными замыканиями.
Кстати, просветите, нет ли у него проблем с функциями возвращающими функции. Не путает ли он результаты с продолжениями?
Не будет, смотрите исходники. Внутри trampoline сводится к тому же recur, оборачивающий всю функцию.

Чтобы вернуть функцию, достаточно обернуть её в любое другое значение — об этом прямо написано в документации. Поэтому нет, не путает.

Никто не спорит, что автоматические оптимизации — это хорошо. Но вопрос был не в этом, вопрос был в том, почему возможность явно вызвать TCO и получить ошибку компиляции, если это невозможно, — это плохо?
Внутри trampoline, конечно же, никаких аллокаций, но для того, чтобы им воспользоваться надо создавать и возвращать на каждом вызове функцию.

То, что приходится оборачивать (а значит аллоцировать в куче) результат, тоже не очень приятно.

Это не плохо если это опция. Но ужасно когда это единственный вариант. Зачем его повторять другим реализациям? Вот и получается, что Clojure (даже ClojureCRL) зависит от JVM, хоть и косвенно.
Вы о чём вообще? Анонимные функции — это всё ещё функции: они компилируются один раз и хранятся в памяти функций (для JVM — это persistent generation), никакого пересоздания на каждом вызове не происходит. Результат надо оборачивать только в одном случае — когда сам вызов trampoline должен вернуть функцию, а это, во-первых, достаточно редкий случай, а во-вторых, значение оборачивается только один раз перед непосредственным выходом из trampoline.
Это в первую очередь замыкания. Они же объекты. Их создавать надо. И память выделять.
Во-первых, загоняться на один дополнительный объект в Лиспе, где новые объекты создаются тысячами, как-то глупо.

Во-вторых, если функция не хранит связанные переменные, то и объект создавать не обязательно. Или вы думаете, что каждый раз при использовании анонимной функции память забивается новым объектом?

Ну и в-третьих, какое это имеет отношение к recur и TCO?
В контексте TCO и надо «загоняться». Мы же говорим об оптимизации, а не рекурсии вообще.

Если не хранит переменных (нет замыкания), то это вызов без параметров. И что такая функция делает? Ковыряет глобальные переменные?

Не забывайте, что код clojure транслируется в байткод JVM, а в нём функции/методы не являются first class citizens. Так что объекты там будут создаваться. Каждое создание анонимной функции создаёт объект-потомок класса clojure.lang.AFunction.

Самое прямое. Clojure не в состоянии сделать эффективную TCO. Предлагаемый trampoline просто заменяет одну проблему другой.
> И что такая функция делает? Ковыряет глобальные переменные?

Кладёт переменные на стек, как бе.

> Каждое создание анонимной функции создаёт объект-потомок класса clojure.lang.AFunction

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

> Clojure не в состоянии сделать эффективную TCO.

Clojure решает проблему TCO для 2 наиболее распространённых случаев: раскрутки в цикл (через recur) и взаимной рекурсии (trampoline). Остальные оптимизации остаются на усмотрение платформы. Да, JVM не умеет делать TCO, и это плохо — но с этим никто и не спорит. Но почему вы считаете, что этот недостаток каким-то образом влияет на имлементации на других платформах? Не забывайте, что Рич изначально разрабатывал Clojure также и для CLR, где хвостовая рекурсия вполне нормально оптимизируется.

> Предлагаемый trampoline просто заменяет одну проблему другой.

Какой другой? Пока что вы только говорите про выделение дополнительной памяти, которое непонятно откуда берётся.
> Кладёт переменные на стек, как бе.

И по выходе из функции их теряет.

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

Класс загружается один раз, а его экземпляры создаются постоянно, на каждом вызове.

Recur это замаскированный цикл. Уж лучше бы предложили использовать map/reduce чем это убожество.

Даже если я буду использовать ClojureCLR, я все равно буду вынужден использовать recur/trampoline ради переносимости на ClojureJVM. Если же пользоваться особенностями реализации, то получится такая же фрагментация какая была у Lisp до Common Lisp. Какбы язык один, но программы не переносимы.

> Пока что вы только говорите про выделение дополнительной памяти, которое непонятно откуда берётся.

Да что тут непонятного? Можете убедится на самом простом примере.

(declare f g)
(defn f [] #(g))
(defn g [] #(f))

Здесь функция f транслируется в код аналогичный такому:

return new clojure.lang.AFunction() {
public java.lang.Object invoke() {
return g();
}
};

Это не точный код. Я его немного упростил, реальный сложнее.
> И по выходе из функции их теряет.

Если они не нужны, то да, теряет. Если нужны, то создаётся отдельный объект, который их хранит — таким образом получается полноценное замыканиие. Но тут уж извините, без выделения памяти под environment создать замыкание ни в одном диалекте Лиспа и ни на одной платформе не получится. А в остальном в Clojure всё достаточно хорошо оптимизировано.

> Класс загружается один раз, а его экземпляры создаются постоянно, на каждом вызове.

Соответственно, это утверждение тоже неверно.

> Recur это замаскированный цикл. Уж лучше бы предложили использовать map/reduce чем это убожество.

TCO для аналогичных случаев — это, как бы, тоже цикл. map/reduce здесь ни причём — это более высокоуровневые конструкции, которые в частности можно реализовать через recur/TCO (обратное не всегда возможно).

> Даже если я буду использовать ClojureCLR, я все равно буду вынужден использовать recur/trampoline ради переносимости на ClojureJVM.

Ну ок, с trampoline я ещё могу понять недовольство — обычные взаимно рекурсивные функции поудобней будут. Но к recur то какие претензии? Вам просто не нравится писать слово recur вместо имени функции? Других отличий, ни синтаксических, ни внутренних, просто нет.

> Какбы язык один, но программы не переносимы

Ну так пишите с использованием recur и trampoline и программы будут полностью переносимы. Лениво — пишите без них и программы всё равно будут работать, хоть и с забиванием стека. А вот как раз на Common Lisp нельзя расчитывать ни то, что на TCO, а даже на некоторые стандартные вещи — при каком там уровне оптимизации в SBCL включается TCO (в стандарте таки ничего не прописано про неё)? А для x64 под Винду SBCL уже допили? А сколько в реальном коде приходится вставлять ридер-макросов для компиляции под разные платформы? (если вы такого не делаете, для интереса попробуйте запустить свой код под Allegro CL — узнаете много нового).
Вы никогда в жизни не сможете оптимизировать код под все возможные платформы, как бы не старались. Стандарты дают гарантию того, что программа будет работать, но ничего не говорят о том, как это будет реализовано. Сделайте язык с расчётом на TCO и попробуйте его запустить на платформе, которая эту оптимизацию не поддерживает — вот это будет действительно проблема. А использование recur вместо имени функции и неудобного trampoline для редкого случая взаимно рекурсивных функций — это всего лишь проблема субъективной оценки, которая решается привыканием.

> Да что тут непонятного? Можете убедится на самом простом примере.

Тю, и из-за этого весь шум? Эти объекты благополучно удаляться вместе с сотней других, пораждённых за время работы функций. Более того, современные JVM очень неплохо оптимизируют код, так что высока вероятность, что объекты будут закешированы и обновляться будут только переменные. Если хотите гарантий, то оберните весь trampoline в let с атомами и действительно обращайтесь к ним как к глобальным переменным. А если вас беспокоит переносимость, так на других платформах же и trampoline можно реализовать иначе, например, заставив его раскручивать алгоритм в обычный взаимно рекурсивный — найти лямбда-функции без параметров даже проще, чем хвостовые вызовы, так что можно реализовать прямо в Clojure через макросы.
Суть TCO в том, что не надо создавать замыкания. И это определяет компилятор. Если взаимно рекурсивные функции делают хоть что-то полезное (кроме работы с глобальными переменными), то у них будут параметры, а следовательно создаваемые объекты-замыкания будут не пустыми. Память будет забиваться ненужными объектами. Это доп. нагрузка на GC.

> Соответственно, это утверждение тоже неверно.

Разберитесь наконец как реализованы замыкания в Clojure.

В Common Lisp есть tagbody/go. С их помощью (и макросами) можно сделать эту оптимизацию даже если её нет в реализации.

Взаимно рекурсивные функции вовсе не редки. Они часто появляются в разного рода парсерах.

> Тю, и из-за этого весь шум?

Посмотрите всё таки код. Никакого кэша там не получится.

>… оберните весь trampoline в let с атомами…

Вы мне предлагаете писать императивно? Ну уж нет!

>… лямбда-функции без параметров…

Ну где вы это взяли? С чего им быть без параметров?
> Ну где вы это взяли? С чего им быть без параметров?

Потому что trampoline требует функции без параметров.

> Вы мне предлагаете писать императивно? Ну уж нет!

Как я и сказал — это проблема предпочтений. Не хотите императивно — ради бога, у вас всё ещё есть выбор между ростом стека, созданием временных объектов, своей чисто функциональной имплементацией trampoline и многим другим. А вы просто привыкли делать это каким-то одним способом и не хотите признавать другие способы.

> Посмотрите всё таки код. Никакого кэша там не получится.

Там — это где? В JVM после оптимизации кода? Оптимизированный нативный код таки будет значительно отличаться от того, что получилось после компиляции в Java код. В частности, что HotSpot, что JRabel будут стараться заинлайнить функции invoke для сгенерированных классов f и g, а также сделать все соответствующие дополнительные оптимизации (всё таки JVM-ы довольно хорошо знают паттерны проектирования, а замыкания — это, как известно, паттерн Strategy). И если они распознают этот паттерн, то вполне могут оставлять объекты для повторного использования или вообще выделить единый scope с глобальными переменными для обеих функций.

> Взаимно рекурсивные функции вовсе не редки. Они часто появляются в разного рода парсерах.

В разного рода парсерах взаимная рекурсия будет применяться к дочерним узлам дерева разбора, в то время как отцовские узлы всё равно не дадут свести алгоритм к хвостовой рекурсии, так что в большинстве случаев никакой оптимизации не произойдёт так или иначе (да и мне сложно придумать ситуацию, в которой разбираемое выражение оказалось бы настолько сложным, что свалило бы стек). Можно, конечно, исхитриться и добиться TCO, ну так можно же исхитриться и ещё больше и свести алгоритм к конечному автомату, так что взаимной рекурсии вообще не будет.

> Разберитесь наконец как реализованы замыкания в Clojure.

Замыкания реализованы в виде классов. Класс может состоять из одного метода или метода и нескольких финальных переменных. Если замыкание ссылается на глобальные (по отношению к своему скоупу) переменные, как, например, в случае с частичным применением функций, то необходимо инстанциировать объект, в котором будут сохранены значения из скоупа. Если же замыкание работает только со своими аргументами (то есть по сути является обычной функцией), то создавать каждый раз объект нет необходимости, и компилятор Clojure использует это преимущество. Другими словами, Clojure инстанциирует объекты замыканий только когда это действительно необходимо.

> Суть TCO в том, что не надо создавать замыкания.

Интересное определение, но всё-таки обычно под TCO понимают оптимизацию, останавливающую рост стека (Педивикия в подкрепление). И на стек же можно класть параметры функции. Создание объектов замыканий, как я уже сказал выше, нужно только в тех случаях, когда функция сохраняет переменные из внешнего скоупа.
> Потому что trampoline требует функции без параметров.

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

>… Clojure инстанциирует объекты замыканий только когда это действительно необходимо…

У меня перед глазами обратное. Декомпилированный пример с двумя функциями f и g показывает что в new вызывается всегда. (Кстати, аналогичный пример на ABCL этого не делает и ведёт себя так как вы описываете, реюзает объекты). Если что, Clojure 1.4.

TCO предотвращает рост стека, но и кучу не использует для этого. Trampoline же просто переносит всё в кучу. Каждому стековому фрейму соответствует объект-замыкание, потому как замыкаются именно фактические параметры вызова функции.
Давайте сначала. Есть 3 варианта использование функций/замыканий в Clojure.

1. Чистые функции, работающие только со своими параметрами, например, такая:

(defn add [x y] (+ x y))


При компиляции такая функия превратиться в статический внутренний класс. Предположим, что у нас также есть функция main, из которой вызывается наша функция add. При компиляции функции main Clojure либо просто поместит ссылку на класс, либо создаст один объект этого класса (из байткода не совсем очевидно, что именно) и поместит в одно из полей класса main ссылку на этот объект, при этом тип поля будет IFn. В этом случае вызов функции add превратиться в обычный invokeinterface по готовому классу — никаких новых объектов создано не будет, и куча, соответственно, также заполняться не будет. Если add вызывается из нескольких функций, то все соответсвующие объекты получат ссылку на тот же экземпляр класса add.class.

2. Функции, замыкающиеся на известные во время компиляции внешние переменные. Например, функция inc, такая что:

(defn make-adder [x] (fn [y] (+ x y)))
(def inc (make-adder 1))


Здесь inc замыкается на значение 1. В этом случае, опять же, будет скомпилирован класс inc.class, в котором будет статическое финальное поле со значением 1. Вызов функции inc из main будет полность анологичен предыдущему случаю — inc.invoke() будет ссылаться на константу, так что создавать экземпляр опять же нет никакой необходимости.

3. Функции, замыкающиеся на внешние, неизвестные во время компиляции переменные. Это как раз случай взаимной рекурсии через trampoline и замыканий без параметров. И вот тут да, параметры заранее неизвестны, поэтому каждый раз будут создаваться экземпляры соответствующих классов f и g из вашего примера. Clojure ничего с этим сделать не может, но это не значит, что оптимизация кода на этом закончена. После компиляции в байткод и загрузки в рантайм JVM применит целый ряд оптимизаций. Вполне возможно, что в итоге создание новых объектов также будет опущено. И даже если этого не произойдёт, как я уже указывал выше, overhead от пары дополнительных объектов в функциональном языке, пораждающем промежуточные объекты тысячами, это не такая большая потеря. На оптимизациях типа использования transient структур данных вы выйграете гораздо больше, чем потеряете на замыканиях в trampoline.
> overhead от пары дополнительных объектов в функциональном языке, пораждающем промежуточные объекты тысячами, это не такая большая потеря.

Ну если глубина рекурсии всего пара вызовов :)

Trampoline это вынужденная мера, а вовсе не прямое решение. И именно JVM вынуждает на эту меру. В этом я вижу негативное влияние JVM на язык. То, что trampoline как-то совместными усилиями компилятора, JIT и чего-то ещё может оптимизироваться, не отменяет того факта, что программисту приходится при написании кода задумываться о низкоуровневой оптимизации (каковой TCO и является).
> Ну если глубина рекурсии всего пара вызовов :)

Внутри самих рекурсивных вызовов будет порождаться множество объектов, скорее всего довольно «тяжёлых», поэтому один дополнительный объект в 12-20 байт не сыграет большой роли. Основная проблема рекурсии — это бесконечный рост стека, который не позволяет сделать её бесконечной или хотя бы достаточно большой. trampoline эту проблему решает. Использование дополнительной памяти — это вопрос производительности (насколько чаще будут происходить циклы сборки мусора), а его, дабы не заниматься преждевременной оптимизацией, нужно начинать решать с более серьёзных затрат памяти.

> В этом я вижу негативное влияние JVM на язык.

Ну тут, опять же, палка о двух концах. В JVM уже давно собираются реализовать TCO, но значит ли это, что после того, как это будет сделано, можно будет исключить из языка trampoline? Вряд ли. Если вы хотите сделать язык переносимым на другие платформы, вы не можете быть уверенным, что на этих платформах будет реализована TCO — возьмите для примера ClojureScript (не то, чтобы это полноценная реализация, но взаимная рекурсия там тоже может быть): откуда вы можете знать, что в браузере пользователя для JavaScript будет реализована TCO? trampoline позволяет реализовать взаимную рекурсию не делая необоснованных предположений по поводу платформы. И Common Lisp, кстати, в этом случае ничем от Clojure не отличается: TCO не входит в стандарт (в отличие, например, от Scheme), а значит расчитывать на неё при написании кроссплатформенного кода нельзя.

> о низкоуровневой оптимизации (каковой TCO и является)

TCO — это больше, чем низкоуровневая оптимизация. Если она гарантирована, то вы можете использовать (правильно построенную) рекурсию вместо цикла, например, для потенципльно бесконечного обмена сообщениями между сервером и клиентом. Если она не гарантирована, то такую логику программы вы надёжно построить уже не можете.
> Основная проблема рекурсии — это бесконечный рост стека, который не позволяет сделать её бесконечной или хотя бы достаточно большой.

Тут неявно присутствует предположение, что стек линейно расположен в памяти. Но реализация VM не обязана так делать. Стек вполне может быть реализован как список на куче, где каждый элемент — фрейм функции. В таком случае разницы с trampoline нету никакой. В одном случае фреймы, в другом замыкания. Накладные расходы на память эквивалентны.

> В JVM уже давно собираются реализовать TCO…

Когда это будет? Развитие JVM несколько замедлилось. Много вещей не вошло в седьмую версию и перенесено на восьмую. Опять не очень приятное влияние на Clojure.

> И Common Lisp, кстати, в этом случае ничем от Clojure не отличается: TCO не входит в стандарт (в отличие, например, от Scheme), а значит расчитывать на неё при написании кроссплатформенного кода нельзя.

CL отличается кардинально. В нём есть tagbody/go. Этого достаточно для реализации TCO на макросах вручную. paste.lisp.org/display/129495 примерно так.
> Тут неявно присутствует предположение, что стек линейно расположен в памяти. Но реализация VM не обязана так делать.

Да бросьте, вся прелесть стека в его скорости, а на списках такую быструю реализацию вы в жизни не получите.

> Когда это будет? Развитие JVM несколько замедлилось.

Вы не уловили мысль параграфа: даже когда в Java всё-таки разберутся с TCO, где гарантия, что на других платформах (JavaScript) TCO тоже будет? trampoline даёт возможность избавиться от такой зависимости. Пользоваться этой возможностью или нет — это уже ваше дело.

> CL отличается кардинально. В нём есть tagbody/go. Этого достаточно для реализации TCO на макросах вручную.

Вручную любой алгоритм с хвостовой рекурсией можно переписать в цикл. Вы почему-то постоянно видите у Clojure проблемы, вместо того, чтобы видеть, как лучше решить задачу в конкретной ситуации. Релизовать сборщик мусора без задержек — вот это проблема. Кластеризировать граф за линейное время — это проблема. А мелкая деталь реализации какой-то фичи языка программирования — это условие, которые при желании всегда можно обойти.

(хм, странно, в отправленных комментарий висит, а здесь почему-то не отобразился)

> Универсальные это такие, на которых кроме лиспа можно запустить что-нибудь ещё.

Лисп-машина тьюринг полна, поэтому на ней можно запустить что угодно. То, что для неё не появилось кучи средств разработки под другие языки, было вызвано её низкой популярностью (а не наоборот, мол, низкая популярность была вызвана тем, что под неё нельзя создать средства разработки под другие языки).

> Я думаю, кроме CLR реализации можно найти и другие такие же заброшенные.

А с чего вы взяли, что ClojureCLR заброшен? То, что Рич сконцентрировался на JVM не значит, что версия для CLR заглохла — ею просто занимаются другие люди.

> ClojureCLR вынуждена повторять всякие особенности JVM и недоразумения вроде recur, не смотря на то, что в CLR прекрасно поддерживается TCO.

Изначально да, recur был сделан как костыль. Но постепенно народ просёк, что recur, в отличие от обычной TCO, сработает гарантированно, либо выкинет ошибку во время компиляции. Кроме того, Clojure-овский loop по определению нельзя рекурсивно вызвать никак, кроме как через recur.

> Думаю, что Clojure сейчас в той стадии в которой Lisp был до того как стал Common.

Эээ. Common Lisp стал результатом работы комиссии по универсализации всего огромного набора лиспов, существовавших на тот момент. Поэтому и получился такой гигант, вмещающий в себя всё. Clojure изначально пошёл по противоположной дороге — простоты и концептуальной целостности, и Рич неоднократно подчёркивал желание побороть те недостатки, которые получил CL в результате унификации.

Чтобы расширить функциональность, достаточно обычных функций. То, что объединяет мультиметоды и протоколы — это диспетчеризация вызовов по типу объекта. Чтобы `(str person)` показывало `Person(Иванов Иван Иванович)`, а `(str car)` показывало `Car(Mercedes Benz)`. Но если целью мультиметодов всегда была именно диспетчеризация (и с этой целью они справляются отлично, позволяя распределять методы по произвольному признаку и любому количеству аргументов), то целью протоколов была замена (и частично исправление) Java-овских интерфейсов со всеми вытекающими — эффективная диспетчеризация по типу, совместимость с Java-овским кодом, введение структурированности и иерархия типов.
Справедливости ради стоит отметить, что в make-hash-table можно передать ключевые аргументы :test и :hash-function, которые позволят вам организовать любые ключи, какие вам заблагорассудится.
Информацию об этом можно почитать в (describe #'make-hash-table)
Можно. Но
1. :hash-function не является стандартным ключом
2. :test по стандарту может быть только eq, eql, equal, или equalp

Т.е. если так делать, то могут возникнуть потенциальные грабли с переносимостью на другие реализации.
Да, вы правы, я по привычке глянул в SBCL, а потом оказалось, что другие реализации в этом плане отличаются.
Вдруг пришло в голову сравнение Лиспа с Эсперанто. Тоже очень изящно, удобно, логично, но вот только большинство говорит на английском(С++), испанском(Java), немецком(PHP) :) И ты со своим Эсперанто можешь общаться лишь с такими же оригиналами :)
Продолжу вашу аналогию. Изучение Эсперанто помогает затем систематизировать знания других европейских языков и облегчает изучение новых.

Так и лисп позволяет пересмотреть свои знания/навыки программирования.

Позволю себе переврать Ломоносова: «Lisp уже затем учить следует, что он ум в порядок приводит».
Вы можете это хоть как-то конкретизировать? Что конкретно человек лучше понимает, потратив немало времени на изучение лиспа? Многие говорят про подобные успехи лиспа, а когда спрашиваешь в чём же суть, все ни бэ ни мэ. Создаётся впечатление что все преимущества лиспа — в том что тем кому нравится его использовать просто тупо нравится синтаксис… К примеру большинство «особенностей» описанных в этой статье выглядят немного странно когда понимаешь что тоже самое есть и в обычных широкоиспользуемых языках, и часто даже лучше реализовано.
Вы не вполне правы. Например, в широкоиспользуемых языках нет маросов, мультиметодов, системы сигналов и ничего даже близко похожего на MOP. И эти вещи действительно меняют подход к программированию.

Если вам нужны конкретные примеры, то с лиспом очень хорошо изучать построение DSL-ей, функциональное программирование, можно совершенно с другого ракурса увидеть ООП. Да что там говорить, даже разные способы применения рекурсии многие люди толком не понимают, пока не познакомятся с лиспом, я это сам неоднократно видел.
Ну вот я и ищу подобные примеры. Можете привести какие-то ссылки или т.д, где показано практически, что могут сделать лисповые макросы чего не могут обычные динамические языки? Те примеры которые я видел реализовывают в лисп фукнционал который в обычных языках и так уже есть…

Хочется поверить в лисп, в нём есть своя красота, своя идея. Но пока практически не вижу никаких преимуществ…
Если нужны примеры того чего нету в других языках, то пожалуйста:

loop: одновременная итерация по неслольким коллекциям;
аналога рестартам нету вообще;
мультиметоды, множественное наследование, комбинирование методов;
macrolet позволяет произвольным образом трансформировать код.

Хватит?
Не знаю, это вам решать, хватит или нет )
Я сначала изучу все примеры что тут написали, потом буду думать. Сейчас, без понимания всего это смысла отвечать нету )
Примеры можно найти в том же Practical Common Lisp (ссылка есть выше), там для иллюстрации работы с макросами автор рассматривает построение нескольких DSL. Очень рекомендую эту книгу, она построена как раз на практических примерах и довольно хорошо освещает ключевые моменты языка.

Есть ещё замечательная On Lisp, но она рассчитана на читателя, уже знакомого с языком. В качестве примеров добавления в язык нового функционала там приводится реализация целого логического языка программирования на макросах (к слову, вся реализация занимает около 200 строк, если я не путаю). Там же рассматривается реализация континуаций (continuation), это интересный механизм, который я вообще кроме диалектов лиспа ни в одном языке не встречал.
Continuations переводится как «продолжения». В scheme в отличие от CL они есть из коробки (call/cc). Но вообще они никак к лиспам не привязаны, даже в руби есть callcc.
Это, действительно, трудно вербализовать, но я попробую. ФП научило писать код минимизируя сайд-эффекты. CLOS дала более глубокое понимание ООП. Рестарты учат что можно отпеделять разные стратегии обработки ошибок. Макросы помогают писать декларативный код.

Синтаксис может нравиться когда он есть. В лиспах же его нету. Точнее, он может быть любым.

Многие вещи действительно есть в других языках, некоторые уникальны. Но только в CL они все в одном месте.
«Изучение Эсперанто помогает затем систематизировать знания других европейских языков и облегчает изучение новых.»
А есть фактические подтверждения этому утверждению? Особенно в части сравнения «изучение эсперанто упрощает изучение последующих европейских языков на х процентов, а изучение [другого языка, скажем испанского] — на y процентов».

Ну и да, мне отдельно интересно, как эсперанто помогает систематизировать знания баскского и польского.
В эсперанто большинство слов заимствовано из романских языков. Так что, думаю, после эсперанто изучать тот же испанский будет проще. Из славянских тоже немало (Заменгоф — поляк, если что). Изучив сначала простой язык с простой грамматикой проще браться за сложный.
«В эсперанто большинство слов заимствовано из романских языков. Так что, думаю, после эсперанто изучать тот же испанский будет проще. „
Проще, чем после итальянского или франуцзского?

“Изучив сначала простой язык с простой грамматикой проще браться за сложный. „
Теоретически, да. А практически — проще браться за язык после изучения другого языка со сходной грамматикой.
«…со сходной грамматикой» тут даже лишнее. Я пробовал учить немецкий до английского (первым) и после. Грамматика не сказать, чтобы очень схожая (после прохождения уровня «мама мыла раму»), но второй язык мне дался гораздо легче.
И во всяких отпусках я сейчас легче и быстрее ухватываю новые куски неизвестных языков (на уровне заказать обед, или арендовать машину), чем раньше.
Мне кажется, эдесь много как чисто утилитарного (общие корни), так и психосоматического.
Вот и я приблизительно о том же — что не важно, какой язык учить (хотя родственные в чем-то полезнее), важен сам факт дополнительного языка.

Так что не вижу особой роли эсперанто в этом вопросе.
Ок. Если не важно какой дополнительный язык, то почему не выбрать самый лёгкий? Эсперанто и разрабатывался таким образом чтобы его легко могли выучить европейцы.
Хорошие языки мертворожденными не бывают. Все попытки скрестить дрель и шуруповерт — жалкое подобие комплекта из дрели и шуруповерта. Так же и с эсперанто.

Эсперанто — хороший аналог КОБОЛа, а не ЛИСПа. Я как-то стихи, помню, на КОБОЛе написал.
«Если не важно какой дополнительный язык, то почему не выбрать самый лёгкий?»
Потому что практической пользы мало.

Знаете английский? Выучите французский. Знаете французский — выучите английский. Знаете оба? Выучите немецкий и испанский.

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

Я не большой знаток иностранных языков, но вот по поводу лиспа вполне определённо могу сказать, что на его примере хорошо изучать концепции, которые применяются повсеместно. Примером тому может служить хотя бы знаменитый курс Structure and Interpretation of Computer Programs, который на примере диалекта лиспа преподносит студентам многие важные идеи, которые лежат в основе программирования.
Лучше сразу математику. Конструктивную, Agda2 например.
Для меня это бесспорное утверждение. :) Функциональный подход позволяет многие алгоритмы выражать гораздо проще и эффективней.
А ещё добавить изучение SmallTalk для понимания истиного ООП и Forth для того, чтобы проникнуться низкоуровневыми конструкциями :)
После этого сразу видно откуда в C# и Java у разных фишек ноги растут.
Кроме смолтоковского ООП стоит посмотреть и на CLOS. Взглянуть с разных сторон.
А касательно форта рекомендую почитать последнюю главу «Let over Lambda», там как раз о реализации форта на лямбдах.
C#, Python, JavaScript тогда какие?
Проблема только в том, что на естественных языках вам нужно говорить с большим числом людей, а в случае языков программирования — только с небольшой группой программистов. Да, возможно кому-то придётся его выучить, но зато общение потом пойдёт гораздо быстрей и эффективней (а не только изящней и удобней).
Community имеет огромное значение в программировании, во-первых, качественно, но также и количественно, посколько первое, хоть и не линейно, но следует из второго.

Невозможно знать какую-то платформу на 100 процентов, поэтому очень часто требуется помощь, примеры, форумы, и т.д. Это просто неотъемлемая часть разработки, сохраняющая уйму времени. Это я уже и не говорю о количестве готовых билиотек, и коммьюнити для них (которые тоже важны, сложные библиотеки это вообще как собственные языки). А общаясь с «небольшой группой программистов» всё время будет уходить на поиски багов, написание библиотек и собственноручному экспериментированию с языком, время которое могло быть потрачено на разработку непосредственно самой программы.
Я про другое — работу в команде над проектом на одном языке. Сообщество у Clojure вполне себе приличное, посмотрите хотя бы количество вопросов на StackOverflow.
Лисп часто рекламируют как язык, имеющий преимущества перед остальными из-за того, что он обладает некоторыми уникальными, хорошо интегрированными и полезными фичами.


Почти все из перечисленного есть в том же питоне или C#. Выделяются только рестарты и макросы, однако это очень спорные фичи. В чем преимущество?
Что там с мультиметодами в C#? Множественное наследование? Фичи спорные потому что их не осилили в C#?
Что там с мультиметодами в C#?

class A
    {

    }

    class B : A
    {

    }

    class Program
    {
        public static void Foo(A a)
        {
            Console.WriteLine("A");
        }

        public static void Foo(B a)
        {
            Console.WriteLine("B");
        }

        static void Main(string[] args)
        {
            var random = new Random();
            for (int i = 0; i < 100; i++)
            {
                dynamic a = random.Next(10) < 5 ? new A() : new B();

                Foo(a);        
            }
            
        }
    }

Множественное наследование?


В C# от него отказались в силу идеологии(оно вносит неочевидность в объектную модель). В питоне оно есть, в С++ тоже. В любом случае явно не экслюзивное преимущество лиспа.

Фичи спорные потому что их не осилили в C#?


При разработке языка программирования мыслят как правило в категориях довольно далеких от «осилим/не осилим». Макросы спорны потому что опять же вносят неочевидность в написание кода. Понять откуда взялось такое поведение и как один кусок превращается в другой в большом проекте с макросами зачастую очень сложно. Это является большой головной болью в С и еще большей болью в системе шаблонов С++, по этому при разработке C# было решено, что проблем от макросов возникает в конечно счете больше, чем пользы. Рестарт же спорен потому, что как правило возникновение исключения означает, что есть какая-то проблема в данной точке вычленения программы, причем она не предусмотрена разработчиком. А значит просто продолжить выполнять тот же участок кода нельзя.

Макросы в C++ и макросы в lisp-мире всё-таки не одно и тоже. Насколько я понимаю, в лиспах они гораздо более продуманы, имеют такой же синтаксис как и всё остальное и такой боли не вызывают. Хотя макросы — good или bad всё равно не совсем понятно…
Ну я по этому и сказал, что спорные фичи. Т.е. нельзя однозначно сказать плюс это или минус.
Без dynamic никак? А если классы не потомки друг друга?

В CL нету никакой неочевидности. Есть method combinations которые эту неочевидность решают. В этом эксклюзивность.

Макросы в CL это совсем не то же самое что макросы C или шаблоны C++.

Отличие рестартов от исключений: если есть проблема, то разработчик предлагает стратегии (в том числе и прервать работу) её решения, а вызывающая сторона решает какую из стратегий применить. Исключения же дают только один вариант — прекращение выполенения.
Без dynamic никак?

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

А если классы не потомки друг друга?

То тоже все будет работать. Потомками они сделаны, чтобы можно было переключиться на статику и показать, что в этом случае вызывается только метода Foo(A a)

Макросы в CL это совсем не то же самое что макросы C или шаблоны C++.

В чем их идеологическое, а синтаксическое отличие?

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

Исключение по определению предполагает исключительную ситуацию, при которой продолжать выполнение бессмысленно. Например мы поделили на ноль — продолжать наши мат. вычисления дальше смысла нет.
В чем их идеологическое, а синтаксическое отличие?
Идеологическое отличие в том, что макрос в лиспе может использовать при компиляции практически любые средства языка и таким образом может порождать произвольный код. Для макросов C и шаблонов C++ это неверно.
Например мы поделили на ноль — продолжать наши мат. вычисления дальше смысла нет.
А если речь идёт не о простейшей ошибке, типа деления на ноль а, например, об ошибке при чтении сложной структуры из файла? Тогда может быть несколько вариантов — пропустить отдельную запись, использовать значение по умолчанию, прекратить чтение. Рестарты в таком случае позволяют легко отделить выбор реакции на ошибку от остального кода и не требуют обязательно прекратить работу, как обычные исключения.
Исключения бывают разные. Самый простой пример — подключение к сервису в интернете вызвало исключение, поэтому надо вместо ответа от сервиса показать сохранённый результат, чтобы юзер ничего и не заметил. Подключение часто может включать в себя много функций и разной логики, и вместо того чтобы проверять каждую отдельную часть, разработчику легче поставить trycatch вокруг всего блока.

И так во многих программах и фреймворках, исключения используются как более удобный возврат результата, а не из-за катастрофических ошибок. Правильно это или нет, ещё можно пообсуждать (хотя я считаю что правильно), но это реальность, исключения используются в неэкстремальных случаях повсеместно.
Я попробовал в вашем коде убрать наследование и он не скомпилировался (mono c# 4.0).

С помощью macrolet можно код не только генерировать, но и трансформировать. cl-cont добавляет с помощью макросов в язык то. чего изначально нет — продолжения. Макросы компиляции позволяют проводить оптимизацию на стадии компиляции. Макросы чтения позвоялют вводить лексические конструкции. Ничего из этого такстовые макросы/шаблоны не могут.

Не стоит быть категоричным. Это может зависить от задачи, поделили на ноль — получили 1e100. У рестартов сфера применения шире получается.
Я попробовал в вашем коде убрать наследование и он не скомпилировался (mono c# 4.0).

 class A
    {

    }

    class B 
    {

    }

    class Program
    {
        public static void Foo(A a)
        {
            Console.WriteLine("A");
        }

        public static void Foo(B a)
        {
            Console.WriteLine("B");
        }

        static void Main(string[] args)
        {
            var random = new Random();
            for (int i = 0; i < 100; i++)
            {
                var a = random.Next(10) < 5 ? (dynamic)new A() : new B();

                Foo(a);        
            }
            
        }
    }
Вас не затруднит сделать всё-таки пример множественной диспечеризации? Чтобы были функции: Foo(A p1, A p2), Foo(B p1, A p2), Foo(A p1, B p2), Foo(B p1, B p2).
Динамическая диспетчеризация(то, что вы называете мультиметодами)
Кстати, это не совсем верно. Мультиметоды — это множественная динамическая диспетчеризация.
dynamic a = random.Next(10) < 5? new A(): new B();
Интересно, не знал о таком. А диспетчеризацию сразу по нескольким параметрам можно сделать?
В C# от него отказались в силу идеологии(оно вносит неочевидность в объектную модель). В питоне оно есть, в С++ тоже. В любом случае явно не экслюзивное преимущество лиспа.
Никто же не говорил, что множественное наследование — это уникальная фишка лиспа. Дело в том, что в лиспе одновременно сочетаются многие интересные возможности. Скажем, множественное наследование в Питоне есть, а вот мультиметодов и макросов нет. А в C++ нет многого другого.
Это является большой головной болью в С
Вот только не надо в одну кучу совать макросы C и лиспа. Это совершенно разные вещи.
Понять откуда взялось такое поведение и как один кусок превращается в другой в большом проекте с макросами зачастую очень сложно.
Эта проблема порождается не макросами самими по себе, а плохим дизайном в принципе. Скажем, точно так же можно реализовать неудачные функции или классы и при их использовании непонятно будет, откуда взялось такое поведение. Что же, надо поэтому от функций и классов отказаться?
Рестарт же спорен потому, что как правило возникновение исключения означает, что есть какая-то проблема в данной точке вычленения программы, причем она не предусмотрена разработчиком.
Рестарты не надо рассматривать исключительно как механизм обработки ошибок. Это обобщённый способ передачи сигналов, и не стоит о нём судить только на основе вашего опыта работы с исключениями в других языках. Есть примеры удачного применения рестартов, можете посмотреть в том же Practical Common Lisp.
Я попробовал модифицировать этот пример и у меня получилось. Множественная диспетчеризация на dynamic'ах таки работает. Правда, классы связаны отношением наследования.
Это здорово. Меня вообще в последнее время развитие C# радует. По сравнению с большинством других языков из мейнстрима он сейчас выглядит довольно привлекательно.
Дело в том, что в лиспе одновременно сочетаются многие интересные возможности


Ну основная мысль была как раз в том, что многое, из того, что перечислено уже есть в том же C#. А если не хватает, можно взять Nemerle, там и макросы и ленивые вычисления аля хаскель и еще куча всего. При этом C#, на мой взгляд, имеет серьезное преимущество при разработке в виде большого сообщества, огромного количества библиотек(да, я понимаю, что лисп может использовать сишные либы, но это немного не то) и удобную систему разработки в виде связки студия+решарпер.
многое, из того, что перечислено уже есть в том же C#
Во-первых, не всё, а во-вторых важно не количество фич, а качество. Вот макросы на мой взгяд в корне меняют подход к программированию. К примеру, вы знали, что основу Common Lisp составляют всего 25 формы, а всё остальное реализовано макросами?

Задумайтесь на минутку: мощная реализация ООП с мультиметодами, комбинацией методов и MOP, рестарты, даже условные операторы и циклы — всё это просто стандартная библиотека, которую вы можете расширять. Захотелось вам поддержку логического программирования — напишите пару сотен строк кода с макросами и готово. Захотелось прототипное ООП, аспектное программирование или даже континуации — пожалуйста.

Даже если для повседневной работы вам хватает нынешних возможностей, скажем, C#, неужели вам не хочется познакомиться с такой гибкой средой? Я, например, тоже не использую лисп на работе, но я его изучил, и с тех пор мой взгляд на программирование сильно изменился.
А если не хватает, можно взять Nemerle
… или лисп. Об этом и речь.
Я ни на чьей стороне, но как часто вы используете мультиметоды (и не могли бы использовать обычные оверлоады) и множественное наследование? (и что сложно было бы достичь простыми интерфейсами или в крайнем случае traitами в каком-нибудь другом языке?) В моей практике может был один случай когда на этом можно было сохранить чуть-чуть времени, но может я просто таких случаев не замечаю потому что не «думаю» в лиспе, не знаю.
Как только познакомился с ними так сразу они мне мерещатся повсюду. :)
Конечно же можно обойтись ценой дублирования кода или потерей производительности.

Один из примеров: есть набор виджетов и набор объектов, их надо маппить друг на друга, причем это не однозначное соответствие. Например значение с выбором можно маппить на радиогруппу или комбобокс. Некоторые виджеты самописные, а некоторые библиотечные, так что Visitor не прикрутишь.

Главное не то, что мультиметоды сокращают время на написание, а то, что они сокращают время на поддержку и не плодят лишнего вспомогательного кода.
Как только познакомился с ними так сразу они мне мерещатся повсюду
If all you have is a hammer, everything looks like a nail.
У вас число pi неправильное. Там на конце пара цифр другие
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории