Почему я остаюсь с Лиспом (и вам тоже стоит)

Original author: Anurag Mendhekar
  • Translation

Зрелый язык может использоваться немногими. Но он остаётся частью моей кодовой базы.

Как давнего пользователя (и активного сторонника) Scheme/Common Lisp/Racket, меня иногда спрашивают, почему я предпочитаю их. К счастью, я всегда возглавлял собственные инженерные организации, поэтому мне никогда не приходилось оправдывать это перед руководством. Но есть еще более важная аудитория - мои собственные коллеги-инженеры, которые никогда не имели удовольствия использовать эти языки. Хотя им не требуются оправдания, они все же спрашивают из интеллектуального любопытства, а иногда и из-за удивления, почему я не схожу с ума по поводу следующей крутой функции, которая будет в этом месяце добавлена в Python или Scala, или что бы там ни было в их вкусе.

Хотя фактическая разновидность используемого мной Lisp изменялась (Scheme, Common Lisp, Racket, Lisp-for-Erlang), ядро всегда оставалось неизменным: язык программирования, базирующийся на S-выражениях, динамически типизированный, в основном функциональный, с вызовом по значению и основанный на λ-исчислении.

Я начал серьёзно программировать в подростковом возрасте на BASIC на ZX Spectrum+, хотя раньше я пробовал писать (вручную) программы на Fortran. Это был определяющий период для меня, поскольку он по-настоящему определил мой карьерный путь. Я очень быстро подошел к пределу языка и пытался писать программы, которые выходили далеко за ограниченные возможности языка и его реализации. Я ненадолго перешел на Паскаль (Turbo Pascal в системе DOS), что некоторое время было забавно, пока я не открыл для себя C в Unix (Santa Cruz Operation Xenix!). Благодаря этому я получил степень бакалавра компьютерных наук, но мне всегда хотелось большей выразительности в моих программах.

Это было, когда я обнаружил функциональное программирование (спасибо IISc!) В Миранде (прекрасной матери уродливого Haskell), и это открыло мне глаза на стремление к красоте в моих программах. Моё представление о выразительности языка программирования стало очень стремительно развиваться. Моя концепция того, как должны выглядеть программы, теперь заключалась в краткости, элегантности и удобочитаемости.

Миранда была не очень быстрым языком, поэтому скорость выполнения была проблемой. Миранда также была статически типизированным языком с выводом типов в стиле Standard-ML. Вначале я был очарован системой типов. Однако со временем я стал её презирать. Хотя это помогло мне уловить несколько вещей во время компиляции, в основном это мешало (подробнее об этом позже).

Примерно через год после этого я закончил изучать языки программирования в Университете Индианы у Дэна Фридмана (известного как «Маленький Лиспер» / «Маленький Схемер»). Это было мое знакомство со Scheme и миром Lisp. Я наконец понял, что нашел идеальное средство для выражения своих программ. И ничего не изменилось за последние 25 лет.

В этой статье я пытаюсь объяснить и исследовать, почему так произошло. Я просто старый динозавр, который не меняет своего образа жизни? Не слишком ли я высокомерен или может быть я пренебрежительно отношусь к новым идеям? Или я просто устал пробовать новое? Ничего из вышеперечисленного. Я нашёл совершенство, и ещё ничего лучше не появилось, чтобы повергнуть этот идеал.

Давайте разберёмся немного. Я сказал это несколькими абзацами выше:

Язык программирования, базирующийся на S-выражениях, динамически типизированный, в основном функциональный, с вызывом по значению и основнный на λ-исчислении.

Я собираюсь начать объяснять это - задом наперёд.

Язык, основанный на λ-исчислении

Основная сущность всех программ - это функция. Функции имеют интенсиональный характер и составляют фундамент процесса проектирования программного обеспечения. Вы всегда думаете о том, как информация обрабатывается, как она трансформируется и как производится. Я ещё не нашёл фундаментальную основу, которая улавливает эту врождённую интенсиональность («как»), лучше, чем λ-исчисление.

Возможно, слово "интенсиональность" сбивает с толку. У математики есть два способа думать о функциях. Во-первых, как набор упорядоченных пар: (вход, выход). Хотя это представление - отличный способ доказать теоремы о функциях, оно совершенно бесполезно при кодировании. Это также известно как "экстенсиональный" взгляд на функции.

Второй способ думать о функциях - это воспринимать их как правило трансформации. Например, умножить входные данные на себя, чтобы получить выходные данные (что дает нам функцию возведения в квадрат, которое на всех языках программирования удобно сокращается как sqr). Это интенсиональный взгляд на функции, который хорошо улавливается λ-исчислением и предоставляет простые правила, которые помогут нам доказать теоремы о наших функциях, не прибегая к экстенсиональности.

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

Языки, основанные на λ-исчислении, позволяют легко «воспроизвести код» в голове. Простые правила λ-исчисления означают, что у вас в голове меньше вещей, а код легко читать и понимать.

Языки программирования - это, конечно, практические инструменты, поэтому их ядро должно быть возможно более простым, чтобы соответствовать максимально широким целям. Вот почему я люблю Scheme (и мой нынешний любимый вариант, Racket - CS, как раз для тех, кто заботится о таких вещах). То, что он добавляет к основному λ-исчислению, - это минимум, необходимый для его использования. Даже когда эти дополнения следуют основным принципам λ-исчисления, есть несколько сюрпризов.

Это, конечно, означает, что рекурсия - это образ жизни. Если вы один из тех людей, для которых рекурсия никогда не имела смысла, или если вы все еще считаете, что «рекурсия неэффективна», то самое время вернуться к ней. Scheme (и Racket) эффективно реализуют рекурсию в виде циклов везде, где это возможно. Более того, этого требует стандарт Scheme.

Эта фича, называемая оптимизацией хвостового вызова (или TCO - tail call optimization), существует уже несколько десятилетий. Это печальный комментарий о состоянии наших языков программирования о том, что ни один из современных языков не поддерживает. Это особенно проблема JVM, поскольку появляются новые языки, пытающиеся использовать JVM в качестве рантайм архитектуры. JVM не поддерживает TCO, и, следовательно, языки, построенные на основе JVM, должны преодолевать препятствия, чтобы обеспечить некоторое подобие иногда применимой TCO. Поэтому я всегда с большим подозрением смотрю на любой функциональный язык, основанный на JVM. По этой же причине я не стал поклонником Clojure.

Это причина номер один. Scheme/Racket - разумная реализация языка программирования, основанного на λ-исчислении. Как вы могли заметить, я не использую слово "функциональный язык" для описания схемы. Это потому, что, хотя он в первую очередь функциональный, он не полностью исключает возможность мутабельности. Несмотря на то, что Scheme не одобряет её использование, он признает, что существуют реальные контексты, в которых может быть полезна мутабельность, и разрешает её без использования вспомогательных трюков. Я не буду спорить здесь с пуристами о том, почему это так, но это связано с тем, о чем я расскажу позже в этой статье.

Вызов по значению (Call-By-Value)

Те из вас, кто знаком с деталями λ-исчисления, возможно, поняли, почему я решил провести это различие. Вспомните мою историю: в отношении функционального программирования мои зубы прорезались на Miranda, который является ленивым функциональным языком (как и Haskell). Это означает, что выражения вычисляются только тогда, когда требуются их значения. Это также то, как определяется исходное λ-исчисление. Это означает, что аргументы функции вычисляются при их использовании, а не при вызове функции.

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

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

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

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

Scheme позволяет вам иметь явное ленивое вычисление за счёт использования thunk-ов и мутаций, которые можно удобно абстрагировать, чтобы у вас был call-by-need, когда вам это нужно. Это подводит нас к следующему этапу.

В основном функциональный

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

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

Как и любой другой инструмент, не-мутирующий код, доведённый до крайности, может быть вредным. Язык, который дает мне разумное использование мутаций для реализации большого объёма кода в более элегантной манере, всегда выигрывает у языка, который заставляет применять (даже хорошую) конструкцию в любой ситуации.

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

Я упомянул монады выше, так что неплохо было бы немного поговорить о них, поскольку они являются чисто функциональным способом получения эффектов. Написав о них докторскую диссертацию, я думаю, что имею некоторое представление о них. Мне нравится элегантность и чистая красота оригинальной концепции монад Eugenio Moggi. Идея отделения вычисления от значения, производимого этим вычислением, а затем преобразование этого вычисления в тип, блестящая во всех смыслах этого слова. Это отличный способ математически понять семантику языков программирования.

Но как инструмент программирования, монады вызывают у меня смешанные эмоции. Это сложный способ изолировать эффекты и затем пропустить их через всю вашу программу, когда вы можете легко создать простые абстракции, которые упростят работу с остальной частью вашей программы. Как однажды сказал выдающийся теоретик типов (который останется безымянным): «Монады полезны только каждый второй вторник».

Монады - это вспомогательные устройства, которые вынуждены использовать функциональные языки, чтобы обеспечить функциональный забор от побочных эффектов. Проблема в том, что забор «заразен», и все, что касается ограды, теперь тоже нужно ограждать и так далее, пока вы не дойдёте до конца детской площадки. Таким образом, вместо того, чтобы сталкиваться с побочным эффектом и элегантно обрабатывать его в абстракции, теперь вам предоставляется сложная абстракция, которую вы вынуждены таскать с собой повсюду. К тому же монады не очень композабельны.

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

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

Динамически типизированный

В современном мире много говорится о типизированных языках. TypeScript считается спасителем в мире JavaScript. Python и JavaScript осуждают за отсутствие статической типизации. Типы считаются важными для документации и коммуникации в крупных программных проектах. Тимлиды бросаются к ногам вывода типов, чтобы защититься от посредственных инженеров-программистов, создающих некачественный код.

Есть два типа статической типизации. Статическая типизация «в старом стиле» используется в C, C++, Java, Fortran, где типы используются компилятором для создания более эффективного кода. Средства проверки типов здесь очень ограничительны, но не претендуют на предоставление каких-либо гарантий помимо базовой проверки типов. Они, по крайней мере, "понимабельны".

Затем появился новый вид статической типизации, уходящий корнями в систему типов Хиндли-Милнера, который породил новое чудовище: вывод типов. Это создаёт иллюзию, что не все типы нужно объявлять. Если вы играете по правилам, вы получите не только преимущества статической типизации старого стиля, но и некоторые новые интересные вещи, такие как полиморфизм. Это взгляд тоже "понимабелен".

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

Однако то, что делают средства проверки статического типа, мешает мне. Всегда. Безошибочно. Как программист, я все время ношу в голове инварианты (причудливое название для свойств вещей в моей программе). Только один из этих инвариантов - это его тип. Иметь инструмент, который может проверить инвариант - это круто, когда вы впервые с ним сталкиваетесь (как я делал с Мирандой).

Но это тупой инструмент. Он может не так много. Итак, теперь у вас есть искусственные правила о том, как удовлетворить этот инструмент. А то, что я знаю, что делать совершенно нормально (и могу оправдать или даже формально доказать для моих вариантов использования), внезапно перестает работать. Итак, теперь я должен изменить свою программу, чтобы удовлетворить потребности ограниченного инструмента. Большинство людей вполне довольны этим компромиссом, и они постепенно меняют свое отношение к программному обеспечению, чтобы соответствовать его ограничениям.

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

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

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

Другими словами, статическая типизация бессмысленна. Она, возможно, имеет некоторую документальную ценность, но не заменяет документацию по другим инвариантам. Например, ваш инвариант может заключаться в том, что вы ожидаете монотонно увеличивающийся массив чисел со средним значением такого-то и такого-то и такого-то стандартного отклонения. Лучшее, что вам может сделать любая проверка статического типа, - это array[float]. Остальная часть вашего инварианта должна быть выражена словами, описывающими функцию. Так зачем подвергать себя страданиям array[float]?

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

Но, как и все остальное, иногда нужно знать типы статически. Например, я много работаю с изображениями, и мне полезно знать, что они представляют собой array[byte], и у меня есть операции, которые будут работать с ними магически быстро. Scheme/Lisp/Racket - все они предоставляют способы сделать это, когда вам это нужно. В Scheme это зависит от реализации, но Racket поставляется с вариантом Typed Racket, который можно смешивать с динамически типизированным вариантом. Common Lisp позволяет объявлять типы в определённых контекстах, в первую очередь для компилятора, чтобы реализовать оптимизацию там, где это возможно.

Итак, опять же, Scheme/Lisp/Racket дают мне преимущества типов, когда они мне нужны, но не навязывают мне ограничения повсюду. Это лучшее из обоих миров.

Базирующийся на S-выражениях

И, наконец, мы подошли к одной из наиболее важных причин, по которой я использую Lisp. Для тех из вас, кто никогда раньше не слышал термин S-выражение, он означает специфический выбор синтаксиса в Лиспе и его потомках. Все синтаксические формы представляют собой атомы или списки. Атомы - это такие вещи, как имена (символы), числа, строки и логические значения. И списки выглядят как «(…)», где содержимое списка также является либо списками, либо атомами, и совершенно нормально иметь пустой список «()». Вот и все.

Нет никаких инфиксных операций, никакого приоритета операторов, никакой ассоциативности, никаких ложных разделителей, никаких висячих else, ничего. Все функции являются префиксными, поэтому вместо того, чтобы говорить «(a + b)», вы должны сказать «(+ a b)», что дополнительно позволяет гибко сказать такие вещи, как «(+ a b c)». «+» - это просто имя функции, которую вы можете переопределить, если захотите.

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

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

Самым большим преимуществом этого синтаксиса является минимализм форм: вам не нужны ложные (spurious) синтаксические конструкции для передачи концепций. Концепции полностью передаются с помощью имен функций или используемых синтаксических ключевых слов. Это производит странно компактный код. Не всегда компактный с точки зрения количества букв, но компактный с точки зрения количества понятий, которые необходимо учитывать при чтении кода.

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

Существует бесчисленное множество интересных синтаксических расширений, написанных поколениями лисперов, включая объектные системы, встроенные под-языки, языки предметной области и так далее. Я использовал это для разработки синтаксических функций, которые позволили мне использовать Scheme для создания программ, которые охватывают весь диапазон от сенсорных сетей до цифровой обработки сигналов и стратегий ценообразования для электронной коммерции. В мире нет ни одного другого языка, который хотя бы приблизился к поддержке такого уровня синтаксического расширения. Это то, без чего я (и множество других лисперов) не могут жить.

Вывод

Язык программирования, базирующийся на S-выражениях, динамически типизированный, в основном функциональный, с вызовом по значению и основанный на λ-исчислении.

Вот почему я все еще использую Scheme/Racket/Lisp и, вероятно, буду использовать его до конца своей жизни. Использую ли я другие языки? Конечно, их много. Но никакой из них не сравнится с этим. Особенно новые. Похоже, что изобретение новых языков - это упражнение, через которое проходит каждое новое поколение плохо информированных инженеров-программистов, когда старые языки намного лучше, чем все, что они могли придумать даже во сне (я представляю вам Ruby, который, хотя номинально имеет свои корни в Лисп, напрашивается вопрос: почему вы просто не использовали сам Лисп).

Как и у всякого предубеждения, у моего тоже есть недостатки. Примерно 15 лет назад все сторонние SDK были написаны полностью на C/C ++, которые могли легко взаимодействовать с Lisp. Появление Java создало этому препятствие, поскольку JVM плохо взаимодействует со Scheme/Lisp/Racket. Это усложняло включение сторонних библиотек в мои программы без выполнения большого количества работы.

Ещё один недостаток заключается в том, что с появлением API в Интернете большинство вендоров выпускают библиотеки на распространённых в Интернете языках (Java, Ruby, Python, JavaScript, а в последнее время - Go и Rust), но никогда в Scheme/Lisp/Racket, если только это не вклад сообщества, которое также редко использует и C/C++. Это часто оставляет меня в положении, когда мне приходится самому создавать уровень API, что, конечно, не очень практично. Racket (мой нынешний фаворит) имеет довольно активное сообщество, которое вносит свой вклад в большие проекты, но обычно оно немного отстает от времени, а когда дело доходит до новейших вещей, я часто остаюсь с тем что есть. Возможно, это основная причина, по которой я буду использовать Clojure в будущем, но это еще предстоит выяснить.

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

И, наконец, проблема производительности. Во-первых, давайте развеем распространённое заблуждение: Лисп не является интерпретируемым языком. Он не медленный, и все реализации имеют множество рычагов для настройки производительности большинства программ. В некоторых случаях программам может потребоваться помощь более быстрых языков, таких как C и C++, поскольку они ближе к оборудованию, но с более быстрым оборудованием даже эта разница становится несущественной. Эти языки являются прекрасным выбором для production-quality кода и, вероятно, более стабильны, чем большинство других вариантов, благодаря десятилетиям работы над ними.

Я признаю, что изучение Scheme/Lisp/Racket немного сложнее, чем изучение Python (но намного проще, чем изучение Java/JavaScript). Однако, если вы это сделаете, вы станете гораздо лучшим программистом и научитесь ценить красоту этих языков так, что ничего другого будет недостаточно.

Anurag Mendhekar (Tech Entrepreneur and Software Artist)

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 136

    +5
    … and You Should Too

    Нет, ты.
    • UFO just landed and posted this here
        +15
        >Появление Java создало этому препятствие, поскольку JVM плохо взаимодействует со Scheme/Lisp/Racket.
        Хм. Ему же все равно, какой из диалектов лиспа применять, он сам это написал. Ну взял бы кложу, и не мутил воду?

        И вообще, автор (я вижу что это перевод, претензия не к переводу) фанат лиспа в плохом смысле этого слова. Мне было достаточно вот этого, например:

        >Другими словами, статическая типизация бессмысленна.

        чтобы перестать ему доверять. Он не видит реальные недостатки, и он не объективен. При этом я лисп сам люблю, пользовался, и все такое.
          +1
          Там далее он упоминает, что собирается попробовать Clojure, и вероятно добавит её к своему стеку.

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

          Я сделал этот перевод специально для того, чтобы разнообразие мнений спровоцировало обсуждение
            0
            >Там далее он упоминает, что собирается попробовать Clojure, и вероятно добавит её к своему стеку.
            Ну да, а еще пишет, что там нет ТСО. Я могу это понять, это аргумент разумный.
              0
              Меня беспокоит, что человек, который защитил докторскую на монадах, считает статическую типизацию бессмысленной и довольно категорично это утверждает. Я не питаю иллюзий относительно своих знаний — они, разумеется, куда меньше, поэтому хочется быть уверенным, что все его резоны достаточно внимательно рассмотрены
                +1
                Ну, у меня есть простое предположение. И исхожу из того, что есть разные проекты и задачи. Возможно у него такие задачи, где статическая типизация слабо помогает.

                Ну так, условно немного — если у вас в задаче скажем 90% это взаимодействие с внешней системой, типа СУБД, где в общем-то нет статической типизации, потому что никто не мешает сделать ALTER TABLE… и поменять набор, названия и типы данных колонок, и при следующем запросе вы будете иметь дело с данными другого типа (обычно так никто не делает, но случаи тем не менее бывают разные), то конкретно в этом месте вам типизация помогает мало. По сути, вы работаете с чем-то типа строки, иногда преобразуя ее во что-то еще.

                Скорее всего, автор просто имеет в виду самого себя, просто забывает на этом акцентировать внимание.

                Ну или другими словами — есть же задачи, которые более-менее решаются языками типа unix shell, где типов данных тоже нет никаких. Замените шелл на lisp — и весьма вероятно, станет лучше. Главное — не обобщать это на другие типы задач :)
                  +3
                  Ну или другими словами — есть же задачи, которые более-менее решаются языками типа unix shell, где типов данных тоже нет никаких.

                  И это ужасно.

                    –1
                    Ну в общем да. Если меня спросить — то лучше на шелле не писать вообще ничего, кроме самого минимума. Но я в том смысле, что есть задачи, для которых и таких языков как-то хватает.
                      +2

                      Ну почему? Если нужно автоматизировать последовательность действий, в которой нет ветвлений, повторений и тому подобной нетривиальной логики, то шелл — самое оно.

                        +3
                        И еще данных сложнее тривиального одномерного массива. Ну т.е. я бы не сказал что это прям ужасно, но надо четко понимать ограничения такого языка.
                          +1
                          Я подумываю написать автору и спросить про решаемые им задачи. Может станет более понятно (ну или мы будем удивлены)
                            0
                            Ну, судя по его послужному списку, это не похоже на какой-то глупый вброс.

                            Я бы скорее сказал, что автор отошел от программирования, и занимается например бизнесом, а лисп это для него хобби — а хобби может быть любым.

                            Ну, как пример — Пол Грэм, насколько я понимаю, сегодня занимается тем, что выращивает стартапы. Вполне возможно уже лет 10 ничего не пишет ни на каком языке (это исключительно предположение, не более). Не практик, иными словами. Стоит ли верить его текстам про лисп, да еще и написанным 15 лет назад? Ну или точнее так — стоит ли им безоговорочно верить? Ведь опыт любого человека ограничен, даже самого крутого специалиста.
                              +2
                              Среди комментариев к оригинальной статье я нашел его довольно итересный ответ, который думаю стоит тут перевести:

                              "… я понимаю, что моя позиция несколько противоречит людям, которые любят статичные типы, и всегда вызывает оживленные дискуссии. В конце концов, я пришел к этому после многих лет работы с Мирандой, и даже сегодня я много работаю со Scala. Итак, я хорошо знаком с последними и лучшими достижениями в области статической типизации, но в конце концов, сравнивая Scheme с Miranda и Racket с Scala, я не могу не прийти к выводу, что статическая типизация ничего не дает для меня или для инженеров проектов, которые мы берем на себя. Во всяком случае, разработка на Scala идет намного медленнее без каких-либо гарантий качества.
                              Хотя я говорю только из личного опыта, другие пытались применить к этому более научный подход. Я столкнулся с этим докладом, где докладчик на самом деле потратил некоторое время на измерение эффектов статических и динамических типов. Доклад долгий (почти час), но он хорошо говорит и поднимает некоторые интересные моменты. Ничего сверхубедительного, конечно, но заставит задуматься. vimeo.com/74354480 "

                              Таким образом, я вижу основания предполагать, что автор статьи имеет некоторое представление о типах и не находится в пузыре «особых» задач, где типы не приносят пользы. Т.к. это может оказаться несколько «confused», думаю, нужно распросить его о его причинах такого мнения.

                              Из этих комментариев я также узнал что Дэниел Фридман написал две «общедоступные» книги о типах: «The Little Prover» and «The Little Typer.». (Это, конечно отсылка к «The Little Schemer» и The «Seasoned Schemer». Пожалуй я добавлю их к своему списку чтения, поближе к его началу). Автор статьи говорит что хорошо знаком с этими книгами, поэтому, я думаю что он куда квалифицированнее чем я может судить о типах. Он считает, что типы «требуют слишком много работы прямо сейчас, чтобы быть полезными в повседневной жизни». Он также ссылается на соответствующее исследование: web.cs.ucdavis.edu/~filkov/papers/lang_github.pdf которое я еще не читал.

                              Вот его взгляд, который он раскрывает в одном из комментариев:

                              «В моем случае я считаю, что типы могут быть чрезвычайно полезными при оптимизации программ, и я рад объявлять их, по мере необходимости, там где такое преимущество может быть очевидным.
                              Другие ограничения, которые требуются для проверки типов там, где я не получаю (или не нуждаюсь) в этом преимуществе, заставляют меня вводить (обычно) ненужные вставки и прогнозы, которые влияют как на мою скорость программирования, так и увеличивают количество мест, где можно сделать ошибки. Ирония заключается в том, что, конечно, средство проверки типов их поймает, но я буду недоволен тем, что я просто зря потратил время на то, что мне изначально не нужно было делать, а теперь мне нужно потратить еще больше времени чтобы исправить это! Так что вместо того, чтобы сэкономить время, мне теперь нужно еще больше времени на выполнение работы.
                              Я также строгий тестировщик, поэтому тестирую каждую функцию снизу вверх, пытаясь получить как можно больше покрытия из моих тестов, особенно с отрицательными тестами. Я делаю это независимо от того, статическая типизация или нет. Это означает, что для меня предполагаемая проверка типов во время компиляции дает почти нулевую выгоду, но имеет высокую стоимость, как с точки зрения времени разработки (которое я могу объективно измерить), так и с точки зрения беспорядка в программе (что несколько субъективно, но большинство инженеров будут соглашаются, когда видят это). Поэтому я отказался от статической типизации. Это не стоит усилий.»

                              Он также провел собственное исследование, которое я тоже приведу здесь:

                              «Чтобы немного встряхнуться, я взял программу в Racket, над которой работал два года, она имеет обширный набор тестов, так что я знаю, что она функционально правильная. Я решил, что возьму один из модулей сегодня вечером и попробую типизировать его (что очень легко сделать в racket, поскольку вы можете переключиться с нетипированного на типизированный вариант, изменив одну строку в своей программе). Чистые накладные расходы на выполнение проверки типов составят мои общие затраты на использование средства проверки типов, но ее выгода будет равна нулю.

                              По сути, это векторная библиотека с множеством функций высшего порядка для машинного обучения. Таким образом, этот модуль не очень большой, всего около 256 строк с примерно 150 строками реального кода, с дополнительными 100 строками кода с примерно 50 тестами. Возможно, мне потребовалось бы 3-4 часа, чтобы написать код и в целом написать для него тесты. В racket очень легко преобразовать из нетипизированного в типизированный, поэтому я сделал преобразование. Средство проверки типов Racket в значительной степени современное, так что это ни в коем случае не примитивный инструмент.

                              Средство проверки типов выдало мне 74 ошибки без какой-либо видимой мне причины (большую часть времени он не мог определить типы, поэтому он хотел, чтобы я объявил некоторые вещи, в некоторых других случаях требовалась дополнительная реструктуризация и объявления). Предполагая, что обработка каждой из этих ошибок занимает примерно 5 минут, я оцениваю общие накладные расходы в 370 минут, что составляет примерно 6 часов. Давайте будем щедры на мою способность отлаживать ошибки типа и возьмем половину этого времени. 3 часа. то есть время, затрачиваемое на доставку модуля, удвоилось, а выгода, которая у меня есть, равна нулю. Итак, прямо сейчас у моего средства проверки типов чистые отрицательные накладные расходы составляют 50%. Даже через 1 час (что, по моему опыту, является исключением, поскольку для поиска ошибок типа требуется достаточно глубокое отслеживание кода), это будет значительными накладными расходами в размере 20%.

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

                              Я очень рекомендую вам самому попробовать подобные эксперименты. Мы сможем сравнить записи в будущем. Но вам придется использовать такой язык, как Racket, чтобы легко переключаться с типизированного на нетипизированный и обратно… Кто знает, возможно, вы никогда не вернетесь обратно к типизированному :)
                                +1
                                >Во всяком случае, разработка на Scala идет намного медленнее
                                Чем на лиспе? Хм. Ну я не знаю, у меня примерно одинаковый опыт в лиспе и скале, и я бы не подтвердил такое утверждение.
                                  0
                                  Я думаю, вы недостаточно квалифицированы… в лиспе. Это провокация, не принимайте всерьез :) Или наоборот, принимайте всерьез, тогда нас ждет интересное обсуждение дальше )
                                    +1
                                    Я же пытаюсь сравнивать, и Scala у меня тоже не основной язык. Ну т.е., опыт в них у меня примерно одинаковый (правда, не считая того, что на скале я начал писать лет на 10 позже, то есть я сам был опытнее на тот момент).

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

                                        А потом еще делал совсем другую вещь — средство проектирования мебели (шкафов купе). Ну т.е. тоже рисуете шкаф, выбирая материалы и наполнение, а на выходе получаете спецификацию и чертежи.

                                        Могу сказать, что в какой-то момент в AutoDesk решили, что лисп им не очень нравится, и нужно разрешить писать на других языках — и в итоге там появилась поддержка .Net — и кое-что на C# было делать несколько проще, или так скажем — привычнее. Но в целом я все равно предпочел бы лисп. Для такого рода задач.
                                          0
                                          Я думаю это очень неревантный опыт. Я бы даже сказал что для «программирования» он практически не должен учитываться, т.к. важные лиспо-концепции отстутствуют в АвтоКаде. У них там просто скриптинг в оболочке скобочек (могу ошибаться, т.к. не сам изучал)
                                            0
                                            Важные концепции? Это какие же?
                                              0
                                              макросы, condition/restarts, мультиметоды из CLOS, много их…
                                                0
                                                Ну, это конечно хорошие вещи все, но я бы не сказал, что они являются прям жизненно важными для лиспа. Во всяком случае, я бы считал фундаментальными те вещи, который были изначально у Маккарти. А остальными — то, что можно реализовать самому, имея в наличии только базовые. Или то, без чего вообще можно программировать — а это в общем-то очень маленькое подмножество для лиспа.

                                                А то может получиться, что и скажем кложа — тоже неревантна, потому что она не CLOS?
                                                  0
                                                  Это совсем неверно. Это как считать что есть жизненно-важная концепция указателей в изначальном си, а остальное можно реализовать самому и получить С++ 20. Или даже джаву. Ну… формально можно, на практике никто этим заниматься не будет.

                                                  Лисп прошел 60 лет развития, лисп МаКарти и современный лисп — это как палка-копалка в сравнении с современным комбайном.

                                                  Кложа определенно проигрывает Common Lisp. Но это не ее вина, она вынуждена приспосабливаться к JVM
                                                    0
                                                    Ну, как неверно. Если у языка есть ядро, на котором можно написать все остальное, и это будет несложно и эффективно — то это ядро и есть самое важное в языке. Во всяком случае, такой взгляд на вещи существует, и имеет какое-то право на жизнь.

                                                    Понятно, что если я при этом никогда не пользовался лисповскими макросами — то да, я конечно же их не знаю. Но по сравнению с человеком, который вообще на лиспе не писал — я их освою сильно быстрее. Или даже — смогу реализовать сам, если уж приспичит.
                                                      0
                                                      Вы ошибаетесь. Для реализации всех этих возможностей вам придется приобрести квалификацию разработчика языка, думаю на это уйдет от пяти лет. Это лишь кажется простым
                                                        0
                                                        Ну, как бы это сказать… это же в лиспе у меня опыта года четыре, а вообще-то его лет эдак за 40. И интерпретатор лиспа, во всяком случае простой, я бы написал бы спокойно, была бы такая задача поставлена.
                                                          0
                                                          Я тоже так думал. Действительно, простой интерпретатор я написал почти сразу, это легко, особенно если делать его на языке со сборщиком мусора. А вот чтобы сделать современный лисп, да еще и компилируемый я учусь уже несколько лет и понимаю, что это совсем непросто. Фичи этого языка очень сбалансированы и тщательно увязаны друг с другом, я рекомендую прочесть (даже по диагонали) lisper.ru/pcl для погружения в тему.
                                                            0
                                                            Так а кто говорил, что я в процессе не буду подсматривать в умные книжки :)? Буду, обязательно.
                                                              0
                                                              Тогда предлагаю объединить усилия ) Вместе заниматься разработкой языков куда интереснее!
                                  +2
                                  В конце концов, я пришел к этому после многих лет работы с Мирандой, и даже сегодня я много работаю со Scala. Итак, я хорошо знаком с последними и лучшими достижениями в области статической типизации,

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


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

                                  Зависит от типов. Лёгкая верификация не требует почти никаких усилий, полноценный хардкор уровня доказательств для математической статьи — да, тяжёлая и долгая работа. Но в повседневной жизни большинство людей не пишет математические статьи, верифицировать что-нибудь там GADT'ами или таскать на уровне типов доказательство n < length array достаточно.


                                  А что до скорости разработки — ну, как говорится, YMMV. Я честно пробовал что-то писать на (нетипизированном) питоне. Скорость разработки зависит от числа строк, и при числе, превышающем 50-100 строк, начинает стремиться к нулю (я не вижу типов, теряюсь, начинаю нервничать, грущу и иду дискутировать на хабр за типы вместо того, чтобы превознемогать).

                      –1
                      Есть же интересный пример на тему статической vs динамической типизации. Возьмите ваш статический язык и накишите-ка класс Number. Не Integer, не Float, не Rational, и никакой другой частный случай — но чтобы Number мог быть и десятичной дробью, и натуральной итд.
                      То что в Лиспе есть из коробки, в какой-нибудь Java — не есть возможным в принципе.
                        +3

                        Взял мой статический язык и за 5 минут получил:


                        {-# LANGUAGE RankNTypes #-}
                        
                        data Number = IntegerNum Integer
                                    | FloatNum Float
                                    | RatioNum Rational
                                    deriving (Show)
                        
                        asRatio :: Number -> Rational
                        asRatio (IntegerNum n) = fromIntegral n
                        asRatio (FloatNum f) = toRational f
                        asRatio (RatioNum r) = r
                        
                        liftNumber :: (forall a. Num a => a -> a) -> Number -> Number
                        liftNumber fun (IntegerNum n) = IntegerNum (fun n)
                        liftNumber fun (FloatNum f) = FloatNum (fun f)
                        liftNumber fun (RatioNum r) = RatioNum (fun r)
                        
                        liftNumber2 :: (forall a. Num a => a -> a -> a)
                                    -> Number -> Number -> Number
                        liftNumber2 fun (FloatNum f1) (FloatNum f2) = FloatNum (f1 `fun` f2)
                        liftNumber2 fun (IntegerNum n1) (IntegerNum n2) = IntegerNum (n1 `fun` n2)
                        liftNumber2 fun n1 n2 = RatioNum (asRatio n1 `fun` asRatio n2)
                        
                        instance Num Number where
                          fromInteger = IntegerNum
                          (+) = liftNumber2 (+)
                          (-) = liftNumber2 (-)
                          (*) = liftNumber2 (*)
                          negate = liftNumber negate
                          abs = liftNumber abs
                          signum = liftNumber signum
                        

                        Получил:


                        λ> IntegerNum 10 + IntegerNum 32
                        IntegerNum 42
                        λ> IntegerNum 10 + RatioNum (3 % 4)
                        RatioNum (43 % 4)
                          0
                          Это что?
                            +1

                            Хаскель.

                          +1
                          1. Не уверен, что это правильно. Integer, Float и Rational должен использоваться по-разному в разных ситуациях. Объединить их в один тип мне кажется очень большой архитектурной ошибкой. Всё равно в процессе работы с ним придётся уточнять, что это на самом деле и в зависимости от этого делать разные действия.
                          2. В принципе, это возможно. Тип Object.
                          3. Можно сделать базовый класс Number.
                    +7

                    Именно.


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

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

                      +1
                      У автора есть мнение, оно не обязательно правильное. Особенно если учесть, что правильность может сильно зависеть от среды и решаемых задач. 15 лет назад заявив о том что ООП — чушь можно было неплохо согреться. Это еще не повод не читать дальше, на самом деле
                    +6
                    Давным-давно, один из моих научных руководителей писал тяжелые матвычисления на Reduce (RLisp), потому что арифметики с произвольной точностью на других языках не было. Как только появился libgmp — он спрыгнул на С++ с нечеловеческой быстротой. И программа ускорилась в 10+ раз. Тупо за счет языка.
                    Lisp хорош в некоторых областях, но не во всех. Как обычно, автор хвалит то болото, в котором он самая большая лягушка:
                    Tech Entrepreneur and Software Artist
                    — ну и жонглер скобками :)
                      0
                      Довольно интересно, не знал про RLisp. Это интерпретируемый диалект?
                        +1
                        Компилируемый. Один из первых. Тони Хёрн написал
                          0
                          «Как и большинство старых Lisp, на первом этапе PSL компилирует код Lisp в код LAP, который является еще одним кроссплатформенным языком. Однако там, где более старые Lisp в основном компилировали LAP непосредственно на язык ассемблера или в какой-либо промежуточный продукт, зависящий от архитектуры, PSL компилирует LAP в код C, который будет выполняться на языке виртуальной машины; поэтому программы, написанные на нем, в принципе так же переносимы, как и C, что довольно переносимо. „

                          Интересный способ. Меня интересуют различные методы реализации виртуальных машин, надо будет изучить как у них там все устроено
                        0
                        У reduce были и другие достоинства, кроме арифметики произвольной точности, ради объективности.
                          0
                          Хотелось бы узнать больше
                            +1
                            Ну reduce это же по сути система для задач математики в символьном виде. Сами вычисления это только часть, упрощения выражений, аналитическое решение уравнений, и т.п. В общем-то то, в чем лисп реально себя проявлял всегда неплохо.

                            Если я правильно понимаю, сегодня оно живет тут.
                              0
                              Спасибо, сейчас изучим
                        +3
                        Как давнего пользователя (и активного сторонника) Scheme/Common Lisp/Racket, меня иногда спрашивают, почему я предпочитаю их. К счастью, я всегда возглавлял собственные инженерные организации, поэтому мне никогда не приходилось оправдывать это перед руководством.


                        Главное почему еще использует этот язык.
                          0
                          Еще никого не уволили за Java? ©
                          0

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

                            +1
                            Как может умереть сообщество, если есть разные варианты языков функциональной направленности с их отдельными сообществами и сторонней поддержкой заинтересованных лиц?

                            P.S. Как пример https://racket-lang.org/

                            http://rosettacode.org/wiki/Category:Racket
                            http://rosettacode.org/wiki/Category:Common_Lisp
                            http://rosettacode.org/wiki/Category:Clojure
                            http://rosettacode.org/wiki/Category:Scheme

                            Guile is a programming language

                            Или, как в старой шутке, что «умерло» — не может умереть? :)
                              0

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


                              Ну а умереть может легко: надо привлекать свежую кровь или когда уйдут на покой мастодонты вроде автора некому будет продолжать их дело.


                              P.S. Racket меня больше всего привлекал как раз из-за наличия типизированного диалекта.

                            +3
                            >>> import this
                            
                            Beautiful is better than ugly.

                            На этом этапе лисп остаётся в музее, в разделе между архивом клинописи и коллекцией скобок обитателей древнего Ирана.

                              +1

                              Да ладно? Скажите это разработчикам Grammarly :)

                                +2

                                Возможно, они предпочитают ugly over beautiful. Разные люди имеют разные предпочтения.

                                  +2

                                  А можно как-то аргументировать, что Lisp is ugly? Исключительно из-за скобочек что ли?


                                  Потому что семантика Lisp и Scheme, пожалуй, одна из самых простых, логичных, ортогональных, гармоничных и красивых на сегодняшний день. CL и Scheme — это представители малочисленного семейства промышленных языков, для которых прописана формальная маьематическая семантика. Кроме них я знаю только Java, DOTTY и C-Light.


                                  Собственно, поэтому и вопрос: чего такого ugly в Lisp-ах?

                                    +7

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

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

                                        Это другие скобки. Скобки питона (они там есть, просто представляются как пробелы) описывают блоки. В Lisp'е же скобки фигурируют даже там, где большинство языков прекрасно обходится inline формой. Более того, унификация скобок выглядит элегантно в bnf'е, но фигово в голове.

                                          +1
                                          Ну в общем да. Но я подозреваю, что в значительной степени это дело привычки, но в более новых языках типа хаскеля от множества скобок ушли, при этом сама программа перестала быть данными в смысле лиспа, но я бы не сказал, что стало хуже. Да и то что автор выдает за достоинство, макросы, в общем-то вполне прилично сделали и в языках с совсем другим синтаксисом, т.е. единственная структура в виде S выражения везде для этого получается что и не нужна.
                                            0
                                            думаю, скобки есть (семантически) в любом языке, текст на любом языке можно представить как бинарное дерево морфем, а бинарное дерево можно представить скобками.

                                            например

                                            (
                                             (
                                              (
                                               В
                                               (
                                                Lisp
                                                е
                                               )
                                              )
                                              же
                                             )
                                             (
                                              (
                                               (
                                                скоб
                                                к
                                               )
                                               и
                                              )
                                              (
                                               (
                                                (
                                                 фигур
                                                 ирова
                                                )
                                                (
                                                 даже
                                                 (
                                                  там
                                                  (
                                                   где
                                                   (
                                                    (
                                                     (
                                                      (
                                                       больш
                                                       инство
                                                      )
                                                      (
                                                       язык
                                                       ов
                                                      )
                                                     )
                                                     (
                                                      (
                                                       (
                                                        (
                                                         пре
                                                         красн
                                                        )
                                                        о
                                                       )
                                                       (
                                                        (
                                                         об
                                                         (
                                                          ход
                                                          и
                                                         )
                                                        )
                                                        ся
                                                       )
                                                      )
                                                      (
                                                       (
                                                        (
                                                         in
                                                         line
                                                        )
                                                        форм
                                                       )
                                                       ой
                                                      )
                                                     )
                                                    )
                                                    ит
                                                   )
                                                  )
                                                 )
                                                )
                                               )
                                               уют
                                              )
                                             )
                                            )
                                            .
                                            


                                            в лиспе же наверно можно делать также и отступы вдобавок к скобкам, чтобы понятнее было?

                                            ещё возможно было бы удобно посмотреть на код на лиспе и других языках программирования в виде «дерева». нету ли такого инструмента, чтобы так показывал, для какого-либо языка программирования?
                                          +2
                                          Заметьте, вы легко написали этот текст, а я легко его прочитал. Проблему со вложенностью преувеличиввют те, кто никогда ничего на lisp-ах не писал. Вложенность контекстов есть в любом языке, просто её маскируют. Тем, на самом деле, lisp и хорош: он не скрывает сложность. Поэтому все стремятся её уменьшить за счёт экономных алгоритмов и представлений данных. Поэтому программы на lisp часто одни из самых простых для понимания и чтения. Ну, таков мой опыт. Вы, конечно, не поверите, потому что, конечно, даже не будете пробовать. Ну… Жаль, что из-за предрассудков люди игнорируют такой отличный инструмент. Впрочем, lisp-ы настолько продуктивны, что для поддержки и развития экосистемы много людей и не нужно.
                                            +6

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


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


                                            Насчёт "нескрытия сложности" — это же плохо. Потому что если бы вам вместо лямбда-функций надо было бы разбираться с доступными флагами условного перехода, то программировать было бы сложнее. Я предпочитаю, чтобы язык скрывал всё то, что у него хорошо получается и делал явным то, что он не может сделать сам. (Посмотрите, например, на волшебные трейты Copy и AsRef в Rust — это же как раз пример того, как скрывается от программиста то, с чем компилятор хорошо справится).

                                              –2
                                              Да ладно? Вы серьёзно? Rust, как пример сокрытия сложности? Rust по объёму boilerplate-кода сравним разве только с Си++. И вы путаете невозможность что-то абстрагировать (типа последовательности инструкций в другую конструкцию) со структурной сложностью алгоритма. С абстрагированием lisp-ы отлично справляются.
                                                +1

                                                Я вам только что привёл пример разумного скрытия сложности — Copy и AsRef. Это не мелкая ерунда, это важное свойство структуры данных — можно ли её беспечно копировать или нет. Если можно — компилятор срезает много углов. Если нельзя — у пользователя появляются хорошо проговоренные проблемы для решения.

                                                  0
                                                  А можете объяснить, какая именно сложность здесь скрывается? По мне так, наоборот, здесь создаётся искусственная сложность, с которой потом идёт героическая борьба. При чём, сам способ борьбы выбран тоже очень сложным, не только для пользователя, но и для реализации. До сих пор, ведь, вылезают хитрые баги в borrow checker. Кстати, как там с доказательством того, что система типов в Rust sound? Помнится, грозились на Coq это доказать. Но полгода назад это дело не шибко продвигалось, статьи не брали в журналы (почему, интересно?). Что-то изменилось?

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

                                                  Впрочем, кто я, чтобы кого-то переубеждать? Если Rust для вас работает, отлично. Но и Lisp для многих работает. Это не архивный инструмент. На нём и в наши дни пишут кучу классных приложений: от операционных систем до редакторов музыки для композиторов.
                                                    +2

                                                    Смотрите. У нас есть данные в памяти. В какой-то момент мы хотим с ними что-то сделать. Допустим, у нас дерево и внутри дерева элемент, который мы только что нашли.


                                                    Если у нас тип данных имеет Copy, то мы (компилятор) может их побитово скопировать и мы можем это значение "вынуть" из enum'а или struct'а (который узел дерева) и работать с ним независимо. В момент, когда мы его "скопировали", оно "наше" и что хотим с ним то и делаем.


                                                    Теперь, предположим, что значение внутри структуры — не Copy. Причины почему оно может быть не Copy чуть ниже, а пока что последствия: если значение не copy, то мы не можем его "стырить", т.е. нам надо с ним работать in place. Мы не можем его "передать" чёрти куда (в соседний тред, например), потому что тред будет жить, а struct — уже нет. Мы можем его только borrow, при этом тщательно следить за тем, чтобы не сделать пересекающиеся ref mut, терпеть ругань компилятора и искать методы как с этим жить.


                                                    Почему мы можем не хотеть Copy? Ну, например, потому что данные большие. Если у нас внутри данных там array[f128;128], то будет медленно, причём, неожиданно медленно.


                                                    Или, это может быть специальная такая страшная память, которую нельзя двигать/копировать (DMA, io_uring, you name it). Или, она под Arc'ом и шарится между двумя тредами, и т.д.


                                                    Соответственно, если программисту все эти DMA до лампочки, то он говорит, что у него данные Copy, и работает с данными как на языке высокого уровня. А если, вдруг, оказывается, что нет — есть инструмент. Copy скрывает эту сложность (возню с non-copy memory), оставляя возможность туда нырнуть, если надо.


                                                    Это пример хорошо скрытой сложности.


                                                    Насчёт непротиворечивости и coq — это выше моего уровня, не могу ничего сказать.

                                                      0
                                                      О чём я и говорю. Сначала смешиваются понятия значений и памяти, при чём памяти не только как хранилища значений, но и как io-интерфейса, то есть, того, что работает в модели исчисления процессов, а потом эту ситуацию пытаются исправить. Одно приводящее к повышению сложности решение пытаются исправить другим сложным решением: навороченной системой типов, свойства которой пока туманны.

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

                                                      Иногда создаётся ощущение, что из всей языковой братии только авторы Erlang, Lisp-ов и Go удосужились изучить CSP или pi-исчисление. Остальные же до сих пор пытаются параллелить имманентно последовательную семантику. Из-за чего вся эта боль и возникает.
                                                        +1
                                                        Иногда создаётся ощущение, что из всей языковой братии только авторы Erlang, Lisp-ов и Go удосужились изучить CSP или pi-исчисление. Остальные же до сих пор пытаются параллелить имманентно последовательную семантику.
                                                        Спасибо вам за коммент Люблю почитать, но не имею профильного CS-образования. Я с трудом сформулировал проблему, а вы мне подкинули ключевые слова, по которым можно найти ответ :)
                                            0
                                            Преимущество префиксной и посфиксной нотации перед инфиксной в том, что если вам надо сложить миллион элементов то не надо писать 999999 знаков "+" что упрощает кодогенерацию. А без кодогенерации Лисп — не Лисп.
                                              +2

                                              Мне кажется, что для кодогенерации это как раз не принципиально, не руками же эти "999999 знаков +" пишем.

                                      +2
                                      Beautiful is better than ugly.
                                      Значит магические подчёркивания и self в каждом методе — это красиво?
                                        +2

                                        self в каждом методе — это explicit is better than implicit. При чтении программы понятно откуда оно появилось. Плюс оно же позволяет при всяких извращениях в метапрограммировании и манкипатчинге менять методы класса на чёрти что (и иметь разумное поведение с первым аргументом=self).


                                        Подчёркивания — грубовато, согласен, но лучше, чем если бы магические методы были без них (т.к. их было бы не различить).

                                          –1
                                          При чтении программы понятно откуда оно появилось.
                                          Я вызываю функцию с двумя аргументами, а объявляю почему-то с тремя. В данном случае, достаточно один раз посмотреть/прочитать вводный курс по языку, и больше какого-то объяснения уже не требуется. Зато писать/видеть придётся каждый раз.
                                          Плюс оно же позволяет при всяких извращениях в метапрограммировании и манкипатчинге менять методы класса на чёрти что (и иметь разумное поведение с первым аргументом=self)
                                          И чем это отличается от условного ruby?
                                            +2
                                            Я вызываю функцию с двумя аргументами, а объявляю почему-то с тремя

                                            Вызываешь таки с тремя, просто первый аргумент передаётся через точку.

                                              0
                                              Это внутренняя реализация языка, к которой я к тому же не имею прямого доступа, метод всё равно неявно связывается с исходным объектом.
                                              Python:
                                              class A:
                                                  def __init__(self):
                                                      self.v = 5
                                              
                                                  def a(self, a):
                                                      print('a =', a, 'v =', self.v)
                                              
                                              class B:
                                                  def __init__(self):
                                                      self.v = 7
                                                      
                                              a = A()
                                              b = B()
                                              a.a(5)
                                              b.a = a.a
                                              print(b.v)
                                              a = 5 v = 5
                                              a = 5 v = 5
                                              7

                                              Lua:
                                              a = { v = 5 }
                                              function a.a(self, b)
                                                  print('b = ', b, 'v =', self.v)
                                              end
                                              a:a(5)
                                              b = { v = 7 }
                                              b.a = a.a
                                              b:a(5)
                                              b = 5 v = 5
                                              b = 5 v = 7
                                      0
                                      Всё перечисленное в Питоне есть либо родное, либо в виде внешних, но давно отлаженных костылей. Так что вопрос «зачем» остался нераскрытым.
                                        +1

                                        В питон завезли скорость и отсутствие GIL?

                                          0
                                          Да и да, давно и во множестве вариантов. PyPy, IronPython, Numba…
                                            +1

                                            И в каком из этих вариантов не херится возможности питона к интроспекции и самомодификации? А без этого смысл писать именно на питоне куда-то уходит.

                                              0
                                              Как минимум в последнем.
                                              +2
                                              Насколько я знаю, никакого «быстрого» питона 3 без GIL нет, и перечисленные вами варианты не исключение — в pypy есть GIL, IronPython это python2, а Numba вообще свой диалект.
                                              Я был бы рад, если бы такой вариант был, я изучаю питон недавно и был очень разочарован, когда узнал про GIL.
                                            +1

                                            В текущем проекте использую Guile Scheme, как альтернативу Bash и Python. Удобнее Bash, потому что всё же, когда кроме строк есть другие типы данных, это существенно упрощает работу. Удобнее Python, потому что лучше сделана поддержка Posix и FFI, а current continuations позволяют проще и гибче программировать сложные потоки управления. Ну, и немаловажно, что код на Guile исполняется раз в 15 быстрее. Вероятно, у автора похожие причины.

                                              0
                                              Ну, есть еще просто вкусы в привычки. Я знаю и лисп и питон не идеально и примерно одинаково, и это не основные для меня языки. И если меня спросить, что я выберу — выбор будет в пользу лиспа.

                                              У питона есть свои недостатки, и на мой взгляд, его сегодняшняя популярность — заслуга множества написанных под него пакетов разного назначения. Но если лисп как язык лично для вас не страшен, то выбор между условным numpy/pandas/etc и условным reduce уже будет далеко не таким очевидным. Ну т.е. если считать, что у нас есть два языка, и у обоих есть хороший пакет для решения нашей задачи — то решение уже логично основывать на качестве пакета, а не на языке.

                                              P.S. Поэтому и не раскрыто, мне кажется — автор рассматривает сферический лисп в вакууме, а не задачи, которые на нем хорошо решать.
                                              0
                                              Основная сущность всех программ — это функция
                                              Уже с этой фразы может начаться эпичнейший холивар.
                                                +1
                                                Да зачем холиварить с заведомо ложным утверждением?
                                                0
                                                Круто. Как будто ты на берегу моря, в беседке, прохладный ветерок, вискарик и полное парение в стороне от регистров CPU, шаговых двигателей, гироскопических управляющих органов спутника-фотографа, винтов квадракоптера. Нирвана полная!!!, наслаждение полетом…
                                                  0
                                                  А какая религия мешает на Lisp использовать регистры cpu и крутить шаговые моторы? Common Lisp позволяет пользователям определять произвольные intrinsic-и. NASA, кстати, использовала Lisp на спутниках для планирования миссии и проверки состояния системы с автоматическим восстановлением. А MIT Scheme используется для управления большим массивом радиотелескопов: крутит моторы и наводит тарелки.

                                                  Lisp — древнющий язык, и чего только на нём не делали. И все эти технологии остались доступными в библиотеках.
                                                    0

                                                    "доступными в библиотеках" остались программы на PL1, Algol, Fortran и многих других языках. Человечество любит постоянно все переписывать на разные языки попутно теряя знания. И я полностью согласен с вами в том, что мир подвержен эйфории юнных поколений, которые создают новое только по тому, что не в состоянии понять старое.

                                                      +1
                                                      Иногда, чтобы понять что-то, нужно самому это сделать. Люди так учатся, поэтому переписывание — это нормально, как и поиск новых языков. Молодёжь, вполне естественно, стремится найти новые ниши для себя, в том числе и языковые. А так… Код на Fortran и Lisp используется в научных вычислениях. PL1 и Algol не видел на практике, а с программами на Fortran и Lisp работать приходится регулярно.
                                                  –2

                                                  Возможно, для каких-то задач Lisp и подходит, но для привычной мне бекенд-разработки в вебе или разработки десктопно-мобильных приложений, мне кажется, он не годится.


                                                  Для этих задач необходима поддержка ООП (чтобы можно было описать множество сложных структур данных, в приложениях бывают сотни объектов с десятками полей), взаимодействие с БД через ORM (чтобы не писать рутинные запросы), использование внешних REST API. Характерны большие объемы кода, начиная от 100К строк и выше. Нужны фреймворки, чтобы не изобретать свой велосипед с нуля. Нужны классы, чтобы объединить данные и функции для работы с ними. В Лиспе, как я понимаю, ООП есть, но со страшным синтаксисом (как и сам Лисп). Статической типизации нету, что затрудняет разбор кода. То есть ты видишь функцию и не понять, что ей приходит на вход. Как это узнать?


                                                  Есть ли для LISP IDE с поддержкой перехода к функции/методу по клику? Если есть, как они работают без типизации в коде? Что-то я сомневаюсь, что это возможно.


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


                                                  Более того, в статье намекают на возможность переопределить стандартные функции вроде +. Вот представьте, перед вами проект на 100К строк и где-то в нем спрятано переопределение функции +. Как вы об этом узнаете и как найдете это место? Это же кошмар какой-то.


                                                  Рекурсия бывает нужна (обход деревьев), но за это приходится платить усложнением понимания кода. Код легко понять, если ты сам его написал, а если это кто-то другой засунул рекурсию с 5 аргументами в код из 1000 строк? А? Если можно обойтись без рекурсии — лучше обойтись без нее.


                                                  То ли дело PHP — в нем типизация есть, видно, что именно передается в функцию, и синтаксис привычный, со скобочками. Все читабельно, можно кликом перейти к определению функции. Если у вас мощный компьютер и IDE, то можно найти все места использования класса или функции, переименовать класс или функцию (вместе с местами, где он вызывается). При написании кода вам выдаются подсказки, какие переменные, классы, методы доступны. В сравнении с этим программирование на LISP выглядит как каменный век.


                                                  Для сравнения синтаксиса — вот пример изменения поля объекта отсюда на LISP и его аналог на PHP:


                                                  (setf (slot-value me 'name) "nitro_idiot")
                                                  $me->setName("nitro_idiot")

                                                  Такое ощущение, что автор на Лиспе пишет задачки вычисления чисел Фибоначчи на 100 строк в одно лицо (то есть знает наизусть весь код). Ему бы реальные проекты поделать.

                                                    +8
                                                    необходима поддержка ООП (чтобы можно было описать множество сложных структур данных, в приложениях бывают сотни объектов с десятками полей)

                                                    ООП для этого совершенно не нужен.


                                                    Нужны классы, чтобы объединить данные и функции для работы с ними.

                                                    И это тоже совершенно не обязательно. Есть модули, в конце концов.


                                                    Кстати, в какой класс вы запихнёте функцию, берущую объект-оружие и применяющую его к объекту-противнику? Или надо сделать объект-менеджер?


                                                    Рекурсия бывает нужна (обход деревьев), но за это приходится платить усложнением понимания кода. Код легко понять, если ты сам его написал, а если это кто-то другой засунул рекурсию с 5 аргументами в код из 1000 строк? А? Если можно обойтись без рекурсии — лучше обойтись без нее.

                                                    А мне как раз о рекурсии рассуждать проще, чем о циклах.


                                                    Но в общем я с вами, конечно, согласен, и с тем, что лисп не годится, спорить не буду.

                                                      0
                                                      >А мне как раз о рекурсии рассуждать проще, чем о циклах.
                                                      Мне кажется, тут все еще проще. Есть структуры данных, скажем так, плоские (одномерные, или фиксированной размерности) — массивы например. А есть рекурсивные по определению, скажем деревья. Ну или графы. И рассуждать в терминах циклов о деревьях — такое себе развлечение.

                                                      А для плоских структур цикл вполне годится — причем скажем в js в рамках цикла вполне себе универсально можно работать как с массивом, где структура индексируется целым числом, так и с объектом, где поля именованные. Да в общем-то, это везде так, наверное. Но просто и легко это только до тех пор, как вы не решите залезть внутрь поля, которое само окажется массивом или объектом — т.е. не решите выразить то же дерево в виде имеющихся у вас структур данных.
                                                      +1
                                                      ООП в Lisp поддерживается, но не на уровне ядра языка. Программист может сделать себе любую удобную ему систему объектов. Объекты со слотами не особо популярны на практике, насколько я могу судить. Обычно просто применяют лексические замыкания, этого достаточно в большинстве случаев. Ну, и изменения внутренних состояний — не самая популярная техника. Мало кто будет именно так реализовывать изменение в структуре данных. Основные идиомы в языке другие.

                                                      Объём кода в 100k для программы на Lisp — это очень много. Они обычно гораздо короче программ со схожей функциональностью на других языках. Можете посмотреть на Rosetta Code, например. 120k — это объём операционной системы с драйверами, приложениями, парой игрушек (Doom, Quake), компилятором и текстовым редактором: github.com/froggey/Mezzano

                                                      C Lisp-кодом обычно работают в режиме живого редактирования, через REPL. В качестве IDE достаточно использовать Emacs или ViM с плагинами, которые через REPL взаимодействуют с запущенной программой. Сам runtime и рассказывает текстовому редактору, какие функции в нём есть, какие параметры они принимают (потому что, обычно, функции снабжают online-документацией), подсказывает места объявления этих функций и обеспечивает быстрый переход к их исходным текстам. Редактируется всё вполне комфортно. И для этого не нужны типы.

                                                      Типы особо не нужны ещё и по той причине, что библиотеки в Lisp не оперируют какими-то сложными иерархиями объектов, обычно взаимодействие осуществляется через простые встроенные типы данных: списки, вектора, числа, процедуры. Этого в большинстве случаев хватает. Изредка используют объекты, но когда есть полноценные замыкания, объекты оказываются нужными не так уж и часто.

                                                      Lisp-ы хорошо приспособлены к работе с большими проектами. Там очень мощные средства отладки, и легко можно автоматизировать поиск всех мест, где вызывается процедура. В большинстве случаев эта функциональность уже реализована в IDE. Но довольно просто это сделать и самому: просто навешиваете trace на процедуру и смотрите, откуда и с какими аргументами она вызывается. Гораздо удобнее и практичнее, чем искать это всё в мёртвом коде.

                                                      Процедуры в Lisp редко бывают длинной в 1000 строк. Я бы даже сказал, что никогда не бывают. Потому что на Lisp пишут люди, и им тоже не хочется смотреть на несколько экранов из скобочек. Очень редко можно встретить процедуру длинной больше 100 строк. Потому что люди абстрагируют участки программ. Потому что в Lisp это очень легко делать, а компиляторы хорошо справляются с inlining-ом функций, никто не стесняется кучи мелких процедур. Да и явно рекурсию люди используют, на самом деле, довольно редко, потому что куча рекурсивных схем уже понаписана, и можно вполне комфортно использовать их.

                                                      Большие проекты на Lisp существуют и вполне успешно развиваются. Первое, что приходит на ум: Guix, TeXmacs, Maxima, ACL2, Mezzano, Grammarly, OpusModus и т.д.
                                                        +4
                                                        Типы особо не нужны ещё и по той причине, что библиотеки в Lisp не оперируют какими-то сложными иерархиями объектов, обычно взаимодействие осуществляется через простые встроенные типы данных: списки, вектора, числа, процедуры. Этого в большинстве случаев хватает.


                                                        Ну, это вопрос как раз дискуссионный. Я бы предпочел бы иметь хотя бы что-то типа алгебраических типов данных. И вообще говоря — со статическим контролем, что я вместо правильного типа не вернул/не передал функции какую-то фигню.

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

                                                        Но довольно просто это сделать и самому: просто навешиваете trace на процедуру и смотрите, откуда и с какими аргументами она вызывается.

                                                        А если она не всегда вызывается? Вы за таким занянием сможете провести немало веселых вечеров, пытаясь понять, почему не. Это как раз случай, когда список мест желательно знать статически, при компиляции.
                                                          0
                                                          Ну, это вопрос как раз дискуссионный. Я бы предпочел бы иметь хотя бы что-то типа алгебраических типов данных. И вообще говоря — со статическим контролем, что я вместо правильного типа не вернул/не передал функции какую-то фигню.


                                                          Это довольно сложно объяснить, наверное, получится криво, но я попробую. Я сам до Scheme активно программировал на С, потом на Go, некоторое время на Haskell. Моим первым всегда был позыв начать со структур данных, а не с кода процедур.

                                                          Но на Scheme программирование другое. Вроде как, ты постоянно упрощаешь код. Это следствие обилия скобочек: никому не нравятся скобочки, все хотят упрощать структуру кода. И это автоматическая активность, не надо себя заставлять.

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

                                                          И постепенно начинаешь программировать по-другому. Начинаешь с кода процедур, с языка понятий на котором описываешь решение задачи, а потом только думаешь о конкретных данных. Вот тут это показано: youtu.be/8lhxIOAfDss

                                                          В сложных случаях, когда действительно требуется вариативное поведение, можно сделать варианты. Scheme поддерживает структуры данных, и предикаты для определения, какой вид имеет структура, можно написать assert-ы для параметров, Обычно, это просто else-ветка в конструкции match, где выбрасывается исключение.

                                                          Однако на практике так редко делают. Чаще либо работают через generic-процедуры (это аналог type class), либо меняют код так, чтобы он на вход получал замыкания. Замыкания могут содержать любую логику (а не только варианты записей) и обладать простым одинаковым интерфейсом. Нечто вроде data as folds. Замыкания реализованы эффективно, компилятор выполняет lambda lifting, поэтому работает это без особых накладных расходов.

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


                                                          Найти места вызова просто. Это обычно уже сделано, но и вручную не сложно: можно пройтись по всем модулям, взять выражения для всех процедур и посмотреть, есть ли внутри вызов интересующей нас процедуры.

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

                                                          Таков мой опыт. Lisp, как и Haskell, надо пробовать. Это языки, в которых программирование другое на уровне ощущений. Пока не попробуешь, не поймёшь. Просто вчитываться в чужой код не получиться, нужен активный навык программирования. Привычные языки они, в общем-то, все одинаковые по своей структуре. Синтаксис только разный, а идиоматика одинаковая. В Haskell и Lisp сами идиомы другие, поэтому у многих проблемы с чтением кода на этих языках. Дело совсем не в скобках.
                                                            +1
                                                            >Таков мой опыт. Lisp, как и Haskell, надо пробовать.
                                                            Ну я вообще-то пробовал. У меня минимум четыре года практики во вполне промышленной заказной разработке.
                                                              –1
                                                              Ну, ok. Люди разные, им нравится разное. Кому-то проще на Lisp писать, кому-то проще на Rust, Haskell, C++, NameIt. Я думаю, что языки в не столь отдалённом будущем не будут особо отличаться по гарантиям надёжности кода, эффективности его исполнения или выразительности. Технологии развиваются. Ничто не мешает статически оптимизировать и анализировать динамические языки, а в статические автоматически добавлять рефлексию и инструментализацию. Lisp же останется среди рабочих языков, потому что на нём написаны важные инструменты. Maxima, например,
                                                                +2
                                                                Ничто не мешает статически оптимизировать и анализировать динамические языки

                                                                Райс мешает. Либо вы аккуратно расставляете аннотации и в итоге по факту программируете на некотором статически типизированном подмножестве вашего нетипизированного языка.

                                                                  +1

                                                                  Причём невыразительном и, скорее всего, unsound.

                                                          +1
                                                          >Grammarly
                                                          Я как-то искал их исходники, и не нашел. Все что у них лежит в гитхабе — это вспомогательная фигня. Было бы неплохо понимать, исходя из чего вы делаете вывод, что это большой проект? Большой в каких попугаях и в сравнении с чем?
                                                            +1
                                                            Они сами об этом пишут: www.grammarly.com/blog/engineering/running-lisp-in-production Может быть, врут, конечно. Кто знает?
                                                              +2
                                                              Не, почему врут? Я бы сказал, что оценить масштабы, а особенно саму сложность проекта, без вникания в исходники и сравнения с чем-то похожим, довольно проблематично. Поэтому у них есть какой-то взгляд на вещи по итогам их работы, но у нас вполне может быть слегка другой. Или не слегка.

                                                              Ну вот взять хоть их пример:
                                                              (defun graylog (message &key level backtrace file line-no)
                                                                (let ((msg (salza2:compress-data
                                                                            (babel:string-to-octets
                                                                             (json:encode-json-to-string #{
                                                                               :version "1.0"
                                                                               :facility "lisp"
                                                                               :host *hostname*
                                                                               :|short_message| message
                                                                               :|full_message| backtrace
                                                                               :timestamp (local-time:timestamp-to-unix (local-time:now))
                                                                               :level level
                                                                               :file file
                                                                               :line line-no
                                                                              })
                                                                             :encoding :utf-8)
                                                                            'salza2:zlib-compressor)))
                                                                  (usocket:socket-send (usocket:socket-connect
                                                                                        *graylog-host* *graylog-port*
                                                                                        :protocol :datagram :element-type '(unsigned-byte 8))
                                                                                       msg (length msg))))
                                                              

                                                              Они этим ну не то чтобы хвастают, но все же считают успешным, раз в пост вставили. А по мне — так у меня подобного кода на любом языке найдется похожий пример, и оно будет ничуть не хуже. И примерно того же размера, и того же уровня понимания сторонним человеком. Ну т.е. код как код, ничего такого в нем нет, вообще.
                                                                +1
                                                                Так, наверное. Кто ж спорит? Я возражал только тому, что Lisp пора сдавать в архив. Нормальный рабочий инструмент с кучей полезных возможностей. Они там дальше в тексте живописуют, как через SLIME занимались удалённой отладкой на живой системе. Клёва же. Это интересная возможность.
                                                                  +1
                                                                  Ну так да, я против применимости тоже ничего не имею. Нужно просто фетиш не устраивать из лиспа, и применять по мере необходимости.

                                                                  > удалённой отладкой на живой системе
                                                                  Ну, я этим тоже занимался. Но если честно, за такое лучше надавать по рукам :) чтоб неповадно было.
                                                                    0
                                                                    Вообще-то нет, давать по рукам не стоит.

                                                                    Именно потому что на лиспе (в общем случае) это безопасно — так можно делать.

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

                                                                    Так что есть опасность ошибиться, если подходить к иной технологии с привычной меркой
                                                                      +1
                                                                      Как раз в общем случае это небезопасно. Представьте, что у вас есть пользователи, и у них есть API. Ну там, REST скажем. И вы залезли, и его поменяли через REPL или удаленную отладку. Просто потому, что можете. И никаких следов. И ничего не проверили. Или проверили, но неполноценно.

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

                                                                      Поэтому, системы, где крутятся например деньги, или отвечающие за безопасность, именно поэтому и принятно внедрять таким образом, чтобы оставались следы — чтобы было видно, что в такое-то время, установили изменения (ФИО, IP), и эти изменения можно было бы отследить в идеале до задачи в JIRA, на основе которых их внесли, и до коммита.

                                                                      Любое изменение на живой системе, даже которое вам кажется безопасным, всегда может аукнуться неожиданым образом.
                                                                        0
                                                                        На практике я никогда не буду менять API «на горячую», потому что у изменения API есть специальный процесс, который незачем нарушать.

                                                                        Но я могу зайти на боевой сервер через REPL, создать временный пакет, написать пару аналитических функций, подключить их через хуки к работающей системе, собрать информацию, сдампить ее для алгоритмов машинного обучения, которые будут работать в оффлайне, и удалить временный пакет. И все это — без прогона по пайплайну деплоя, задержек, бюрократии — просто в режиме реального времени. Это может быть очень удобным
                                                                          0
                                                                          Ну я это и в Java могу, и много раз делал. Так что это не достоинство собственно лиспа, строго говоря. И вопрос опасности или безопасности этого процесса определяется скорее тем, есть ли у вас там команда Undo. А если нет — то лучше все же давать по рукам :) Или еще проще — кто сломал, тот и чинит.
                                                                            0
                                                                            Я не знаю и хочу уточнить — могу ли я в Java на множестве живых объектов какого-то класса переопределить метод, не потеряв данные, таким способом, чтобы по мере обращений к каждому объекту старый метод был заменен на новый?
                                                                              0
                                                                              Скажем так — не все изменения возможны. Нельзя менять сигнатуру существующего класса, если упростить. Ну т.е. вы всегда сможете создать что-то новое, но далеко не всегда изменить существующее. Эти ограничения ослаблялись со временем, но они есть.

                                                                              Ну и в общем понятно, откуда ноги растут — если вы изменили существующий класс несовместимым образом, как отреагируют другие классы, которые от него зависят? Это вопрос без очевидного ответа. В вот менять код внутри методов — на здоровье.
                                                                                –1
                                                                                А вот в Common Lisp можно, что создает инженерные удобства. Там все эти проблемы решены на уровне метаобъектного протокола. Это сильно упрощает жизнь
                                                            +1
                                                            C Lisp-кодом обычно работают в режиме живого редактирования, через REPL.
                                                            Основной плюс статической типизации — возможность поменять сигнатуру и получить список всех мест, где нужно что-то поменять. Есть ли такая возможность у лиспа, без необходимости покрывать тестами каждую строку?
                                                              –1
                                                              Нет, но есть кое-что получше — возможность спросить у работающей программы, где все эти места, в которых нужно что-то поменять
                                                                +2

                                                                А она откуда это знает? Это уже как-то проблемой останова пахнет в общем случае.

                                                                  0
                                                                  С чего вдруг проблема останова? Интроспекция на уровне структур данных и CFG плюс удобные инструменты — все то же самое, что обычные программисты делают в IDE, только в рантайме, на живой системе.
                                                                    +1

                                                                    У меня, в принципе, на это в ответ тот же вопрос, что и у alsoijw ниже.


                                                                    Я на лиспе писать не могу, поэтому буду писать на чём-то сиподобном. Вот представьте себе, что у меня есть код


                                                                    int main(int argc, char **argv)
                                                                    {
                                                                      if (argc > 100)
                                                                        doFoo(1);
                                                                      else
                                                                        doFoo(0);
                                                                    }

                                                                    и я хочу поменять doFoo, но программу я запускал с сильно менее чем 100 аргументами. Что будет с вызовом в первой ветке if?

                                                                      0
                                                                      Сценарий, как я понимаю такой:
                                                                      — Пишем doFoo которая принимает аргументы
                                                                      — Пишем main который вызывает doFoo.
                                                                      — Исправляем doFoo, теперь она не принимает аргументов. Если в этот момент main будет вызыван — будет runtime-error.
                                                                      — Поэтому перед тем как обновить doFoo следует найти все его вызов doFoo и исправить их, чтобы они не передавали аргументов. Это можно сделать интроспекцией или просто поиском по сорцам.

                                                                      Но изначально вопрос был про объекты. Впрочем, я вроде бы уже ответил там.
                                                                  +3
                                                                  Если какая-то ветка не исполнилась, то о ней будет сообщено? Нужен пример.
                                                                    0
                                                                    Не очень понял вопрос. Может быть проще воспользоваться диссектом? github.com/Shinmera/dissect?
                                                                      0
                                                                      def a(b)
                                                                      	b.с()
                                                                      end
                                                                      Вот есть код, который работает, например при открытии какого-то меню, и в функцию передаётся объект с методом c. Вот я произвожу рефакторинг и решаю удалить этот метод. Меню это я не открываю, код не исполняется, могу ли я как-то узнать об этой ошибке не нажимая на кнопку и не покрывая каждую функцию тестами(нужно ведь найти каждый вызов a и убедиться в том, что туда передаётся правильный объект).
                                                                        –2
                                                                        > могу ли я как-то узнать об этой ошибке не нажимая на кнопку и не покрывая каждую функцию тестами

                                                                        Да, если спросить интроспективно все места, где метод вызывается. Это можно сделать автоматически, если написать немного кода.

                                                                        Например код-волкер может проходить по обновляемой функции, запрашивать все вызываемые функции и проверять их сигнатуру. Если она не совпадает — прерывать обновление. Не то чтобы мне такое было нужно когда-либо, но я бы написал если бы было необходимо.
                                                                          0
                                                                          Да, если спросить интроспективно все места, где метод вызывается. Это можно сделать автоматически, если написать немного кода.
                                                                          То есть написать типизацию самому.
                                                                          Например код-волкер может проходить по обновляемой функции, запрашивать все вызываемые функции и проверять их сигнатуру. Если она не совпадает — прерывать обновление.

                                                                          class B
                                                                          	# begin
                                                                          	def b
                                                                          	end
                                                                          	# end
                                                                          end
                                                                          
                                                                          def a(b)
                                                                          	b.b()
                                                                          end
                                                                          
                                                                          Вот полный код примера. Если я удалю код между begin и end, то сигнатура a не изменится, и как следствие, ошибку я поймаю либо в рантайме, либо во время компиляции.
                                                                            0
                                                                            Нет, это не писать типизацию самому, это интроспекция + паттерн-матчинг. Сравнительно немного кода, кстати.

                                                                            Что касается примера:

                                                                            При работающем код-волкере ошибка будет при попытке залить def a(b), потому что интроспекция обнаружит отсутствие метода b у класса B.

                                                                            Или, если обновляется класс B — то ошибка возникнет при попытке залить его метод b — потому что есть неурегулированные вызовы к нему.

                                                                            Таким образом все довольно надежно.

                                                                            Это не compile-time и не run-time — а update-time. Ошибка в рантайме может случиться только если вызываемый метод, например b, вычисляется динамически, такое код-волкер не отловит (если его специально не неписали настолько умным). Но если вы вычисляете вызываемый метод, то код вычисления может сам проверить, есть ли то, что он собирается вызывать, благо метаобьектный протокол позволяет заглянуть.
                                                                              0
                                                                              Это не compile-time и не run-time — а update-time.
                                                                              Хорошо, допустим есть такой код.

                                                                              class B
                                                                              	# begin
                                                                              	def b
                                                                              		puts 123
                                                                              	end
                                                                              	# end
                                                                              end
                                                                              
                                                                              def a(b)
                                                                              	b.b()
                                                                              end
                                                                              
                                                                              def c
                                                                              	b = B.new()
                                                                              	a(b)
                                                                              end
                                                                              
                                                                              c()
                                                                              Правильно ли я понимаю, что так-как он выполнился, то среда исполнения знает, что у класса B должен быть метод b, и если я попытаюсь отредактировать этот код, то сразу же получу предупреждение? Но в то же время, если я перезагружу процесс, или передам файлы на другой компьютер, где функция c не вызывалась, то там среда исполнения ничего не сможет сказать? Если да, то как с этим бороться, если нет, то за счёт чего это происходит?
                                                                                0
                                                                                И да, для каких языков это актуально: common|emacs lisp, scheme, racket, clojure?
                                                                                  0
                                                                                  Редактировать можно сколько угодно, предупреждение можно получить при попытке обновить. Это не зависит от того где и когда будет загружено — можно передавать файлы сколько угодно — среда исполнения узнает о методах классов по мере загрузки кода, который их обновляет.

                                                                                  Это происходит за счет метаобьектного протокола (MOP) Common Lisp
                                                                                    0
                                                                                    Довольно сложно понять, как это работает. Как можно найти hello world по данному вопросу?
                                                                                      0
                                                                                      Есть такой hello-world, называется Art of MetaObject Protocol. Только в нем страниц много
                                                              0
                                                              То ли дело PHP — в нем типизация есть, видно, что именно передается в функцию, и синтаксис привычный
                                                              Как обычно я пошел, взял обычный порошок. Синтаксис привычный — результат отличный!
                                                              0
                                                              Правда ли что полноценный REPL есть только у Лиспа? По идее, если ввести, например, в консоль js что-то вроде
                                                              var a = 5; console.log(a+b)

                                                              то будет ошибка, а Лисп всего лишь скажет что надо бы кое-что добавить.
                                                                +1
                                                                Что вы называете полноценным? Был такой язык — Аналитик, на машине МИР-2, так он позволял и то, что вы делаете в js — т.е. можно было вывести a+b, если b еще не определена. И получилось бы у вас 5+b. А когда позже вы определите и b тоже, получите например числовой результат. А если определите b как c+d, то увидите 5+c+d. Короче, вариантов масса, и я бы не сказал бы, что лисповский прям самый лучший или мощный.
                                                                  0
                                                                  Не знаю насколько это «полноценный» в вашем смысле, но лисповый репл позволяет отправлять лисп-системе выделенный фрагмент прямо из любого файла и получать применимый результат. Сказать что это удобно — ничего не сказать
                                                                    0
                                                                    Человек спросил про log(a+b). Ну так вот, Аналитик позволял вывести это выражение, даже если часть переменных неизвестна. А потом доопределить. Это разные взгляды на полноценность. И да, это был 1968 год.
                                                                  +1

                                                                  Ну вот мне мой репл говорит:


                                                                  λ> a = 5 
                                                                  λ> print (a + b)
                                                                  
                                                                  <interactive>:2:12: error: Variable not in scope: b

                                                                  Это полноценно или нет?

                                                                    0
                                                                    Кажется, вы говорите про каррирование и частичное применение. Вот haskell
                                                                    a b c = b + c
                                                                    
                                                                    d = a 5 -- вызов с одним аргументом, это ещё не число, но его можно куда-то положить
                                                                    
                                                                    f = d 4 -- а теперь значение
                                                                    А вот js, не такой красивый
                                                                    a = (b) => (c) => b + c
                                                                    
                                                                    d = a(5)
                                                                    
                                                                    console.log(d(5))
                                                                    А вот мы меняем порядок аргументов
                                                                    a = (b) => (c) => b / c
                                                                    
                                                                    d = (e, f) => (g) => e(g)(f)
                                                                    
                                                                    console.log(d(a, 2)(4)) // 2
                                                                    +1
                                                                    Язык программирования Lisp уроки и задания (видео уроки)

                                                                    ЛШЮП-2020
                                                                    www.youtube.com/playlist?list=PLNdb9-93_ov40Nx17r93vezXebkRNK0x5

                                                                    P.S. Перевод цикла статей. (1-10)
                                                                    Lisp: Слезы радости, часть 1 rus-linux.net/MyLDP/algol/LISP/lisp01.html

                                                                    Only users with full accounts can post comments. Log in, please.