Comments 207
Скобки в Лисп ничем особым не отличаются от скобок в Си каком-нибудь или JavaScript. Почему-то никто не возражает против кучи закрывющих })
в каком-нибудь node-коде или кучи закрывающих тэгов в каком-нибудь xml. Наверное, потому что их пишут в разных строках?
В лиспе легко и натурально «уйти вниз» и в конце просто добить скобками в одну строку, выравнивание только вверху.
В сях/js глубоко не закапываются и принято закрывающие скобки выносить на отдельную строку.
(return (cdar guts))))))))))нашел 10 штук
Да, поленился погрепать, просто поскролил, ошибся. Там ещё в одном месте 7 есть. Но, ведь, всё равно, нет никакой бесконечной глубины вниз. Эти 10 скобок в какой-то особо эпичной по меркам Lisp процедуре. В Си бы это место закрвалось так: );}}}}
. Ну, да, на 4 символа меньше. Но я бы одну скобку засчитал в пользу Lisp, потому что она закрывает блок labels
с взаимно рекурсивными процедурами в локальной области видимости. Си и Go так не умеют. JS как бы умеет, но у него проблемы с областями видимости.
Впрочем, это всё не особо релевантно. Обычно в Lisp закрывающие скобки руками не пишут и внимания на них не обращают, в отличии от...
зачем тогда вообще писать, раз и без них программа остается читаемой? неужели те же правила чтения нельзя прорастить в компилятор языка в виде диалекта?
Так редактировать код намного проще: если нужно взять выражение, то курсор устанавливается на открывающуюся скобку и копируется всё до соответсвующей закрывающейся. От редактора не требуется каких-то особых синтаксических возможностей. Плюс там есть ещё парочка фишек структурного редактирования, которые работают с s-выражениями. Вот, есть демо: http://danmidwood.com/content/2014/11/21/animated-paredit.html
Так структура кода отчётливо видна. Не надо гадать, как всё сгруппировано, какие приоритеты операций и правила переноса выражений между строками. Это освобождает ресурс внимания, можно сосредотачиваться на другом.
Это повышает культуру написания кода, на самом деле. Куча скобочек никому не нравится, поэтому люди стремятся писать простой по своей структуре код, пользуются абстрагированием, выносят общие выражения в процедуры. Сама структура кода становится более ясной и читаемой. Скобочки заставляют лучше продумывать код. И это хорошо.
Нет, на самом деле нет! Lisp — это не отдельная реализация, а ANSI-стандарт (теперь уже Common Lisp). Как и многие другие стандарты (например, ANSI-стандарт для языка программирования Си), он не содержит всего, что вам может понадобиться при написании приложений, например сокетов, синтаксических анализаторов XML, библиотек SQL и т. д.
Ну, вообще-то например Java или .Net содержат большую часть таких библиотек. А еще, если вам в мире Java вдруг потребуется парсер XML, вы с весьма высокой вероятностью найдете подходящий, и сможете его использовать без проблем в своем коде. Причем найдете вы его скорее всего прямо в maven central репозитории, так что подключение к проекту будет состоять из указания координат библиотеки в скрипте сборки.
И в общем-то, мне кажется что на сегодня уже все популярные языки решают эту проблему одинаково — то чего нет в языке из коробки, то есть в репозитории, и есть возможность просто это подключить и использовать.
Большинство авторов этих статей, кажется, серьезно думают, что каким-то образом они будут серьезно восприняты участниками Lisp-сообщества, и что эти участники осознают свои ошибки и начнут предоставлять высококачественные библиотеки для веб-программирования и синтаксического анализа XML бесплатно и сразу.
Так что я вот такие фразы не понимаю. Есть куча других популярных языков, где все именно таким образом и обстоит. Бесплатно и сразу. И еще зачастую непростой выбор между несколькими хорошими решениями для одной задачи. Если лисп сообщество так не считает — разве кому-то от этого хуже, кроме самого сообщества?
Честно говоря, меня смущает такой подход. Складывается впечатление, что это своего рода самоубеждение: мол проектов/библиотек/программистов меньше, чем у других языков, но нам это и не надо! В конце концов, раз такие статьи пишутся, значит авторов не всё устраивает. Но реклама получается специфическая.
Я, как пишущий на "условно маргинальном" языке (rust) очень даже заинтересован в его развитии ведь это будет значить большее количество и лучшее качество библиотек, наличие инструментов, возможность проще найти людей на проект (что в свою очередь увеличит количество проектов).
Да, если я делаю что-то исключительно своими силами, ну или могу взять парочку джунов и вырастить специлистов (которым будет некуда бежать), то это всё не кажется проблемой, но большая ли это ниша?..
Ну, а на самом деле, зачем? Пусть программистов и библиотек меньше. Главные же вопросы: можно на Lisp решить задачу или нельзя, насколько это можно сделать быстро, и насколько эффективным будет код. Опыт показывает, что решить можно, усилий это много не потребует (ну, потому что язык с базовой библиотекой действительно мощный), код будет достаточно быстрым. А мировое доминирование и огромная популярность — это что-то совсем перпендикулярное. Сообщесто Lisp достаточно развитое, чтобы даже писать свои браузеры, операционные системы и компиляторы. Что ещё нужно для пригодности инструмента?
Нужно не много но в хорошем качестве, например
- Иметь возможность расширять команду, с этим у лиспа большие проблемы
- Современная документация с примерами решения задач (начинал с PCL и обалдел от возможности сделать одно и то же 10 способами)
- Набор хорошо поддерживаемых библиотек (в Lisp с этим есть определенные проблемы, как пример веб сервер с хорошей производительностью и набором фич — ssl\websockets\oauth и т.д. желательно без дополнительных приседаний)
и т.д.
А мировое доминирование и огромная популярность — это что-то совсем перпендикулярное.
Ну если цель писать код самому, то ладно. В других случаях, не могу согласиться. Да, популярный язык/инструмент не обязательно хороший, но отсутствие популярности однозначно несёт с собой минусы.
Статья ссылается на Debian Woody, который вышел в 2002-ом и был актуален до 2006-го.
Я согласен, что они правы, что на данный момент нет достаточно хороших библиотек для всех применений. Однако я серьезно сомневаюсь, что решение этой проблемы каким-либо образом повлияет на популярность Lisp.
А я как раз не сомневаюсь. Если посмотреть на ряд популярных сегодня языков, то мы увидим, что почти каждый из них сопровождается солидным репозиторием компонентов, будь то maven central, npm, или там pip. Иногда исходники, иногда скомпилировано. Но всегда просто и стандартно. И чем выше популярность языка — тем как правило больше размеры этого репозитория.
Один из самых солидных репозиториев компонентов на любой вкус у хаскеля, и чё-то как-то на массовость не особо влияет.
На лиспе трудно (невозможно) писать не вникая, а индустрии надо именно это: вон го как хорошо пошел, именно из-за дубовости.
Ну как не влияет? Влияет! Сравните с агдой, где, кажется, тоже можно писать запускающиеся программы, а экосистемы нет никакой. Или там с Clean каким-нибудь.
У агды нечеловеческий синтаксис (который лично мне очень нравится). Начинать изучение языка с построения в голове транспайлера мыслей в клавиатурные шорткаты, которые к тому же работают только в твоем личном хорошо настроенном редакторе — нереальный шоустоппер. Хуже емакса (которые мне тоже раньше очень нравился, а потом я полгода им не пользовался, шорткаты позабыл, и меня это дико взбесило) — но в емаксе хотя бы меню есть, можно мышью нашарить нужную команду.
Сравнивать надо с, например, julia. (Го не годится, потому что если бы не гугл, про него никто никогда бы всерьез не заговорил, js не годится, потому что монополия, и альтернатив нет, и так далее). Экосистема очень так себе, хаскелю во внучки годится, — но найти разработчика, по опыту судя, чуть ли не проще, чем на хаскеле.
Чисто опыт: в Haskell многое заброшено на половине. Автор удовлетворил своё влечение к монадам, и потерял интерес. Поэтому, вроде как, заявлено много всего, но для production подходит мало пакетов. Допиливать же их — отдельное удовольствие, потому что, конечно же, авторы выбирают особо изощрённые typelevel конструкции, которые ещё полгода изучать надо, чтобы понять, что к чему. Слишком тяжело. Поэтому оно всё в таком полусделанном состоянии. Есть набор центральных таких компонент, которые использованы в самом GHC и окружающих его утилитах, но и только. Если в GHC это не используется, то не повезло.
На Lisp-е легко фигачить, не приходя в сознание. Я вот писал или пишу на Си, Java, Go, Python, Haskell, Си++, JavaScript, Bash, Basic, Pascal, Delphi. Из этого всего Lisp — самый простой и мощный язык по моим ощущениям.
Для личного любопытства и статистики: какие заброшенные пакеты вам встречались? У меня в моём опыте, конечно, такое тоже есть (но оно и в других языках есть).
С curl были проблемы, с DetFlow и ещё какой-то библиотекой для управления процессами (не помню названия).
Проблема есть и в других языках, но в других языках проще дополнить функциональность. В Haskell же структура кода жёстко фиксируется типами, и если не понять идею автора, которая может быть весьма заковыристой, изменить что-либо сложно.
Я не сторонник размышлять обо всём с точки зрения бизнеса и трудозатрат, но тут реально тяжело. Надо быть узким спецом именно по Haskell и его расширениям, чтобы дорабатывать чужой код. А если нет у человека интереса в том, чтобы быть таким спецом, то в итоге проще поменять язык.
На это говорят обычно, что вот мол лень тебе учиться, не хочешь освоить крутую штуку. Но вот лично мне надо изучать всякие распределённые системы, алгоритмы оптимизирующей компиляции и байесовские методы в машинном обучении. У меня просто на премудрости Haskell не остаётся сил. И что мне делать?
И я просто беру Lisp, который по ощущениям в 1000 раз проще, но позволяет писать в функциональной парадигме, которая весьма удобна для математики.
Тут мне скажут: надо просто нанять спеца в Haskell. Но у меня нет таких денег. Как на зло, спецы в Haskell стоят дорого, а я не банк.
Вот как-то так получается.
С curl были проблемы, с DetFlow и ещё какой-то библиотекой для управления процессами (не помню названия).
А, ну с байндингами там традиционно всё тяжко. Мне больше всего байндинги к гнуплоту понравились, там имена хорошие.
Проблема есть и в других языках, но в других языках проще дополнить функциональность. В Haskell же структура кода жёстко фиксируется типами, и если не понять идею автора, которая может быть весьма заковыристой, изменить что-либо сложно.
Ну фиг знает. Я приходил в другие кодовые базы и по работе, и по фану — типы как раз помогают очень быстро понять, что происходит в коде, изменить код и убедиться, что ты его не сильно сломал. При этом у меня есть опыт и с более слабыми языками, от C++ до того же питона (в последнем, к счастью, только читать идею надо было) — и это боль, потому что ну непонятно же, что в функцию приходит, что уходит, что она делает, чего она точно не делает.
И при этом я не могу сказать, что я такой уж спец по хаскелю.
А вообще это довольно забавно, ибо, видимо, зависит от мышления. Вот тот же питон вроде как простой и продуктивный, не нужно думать о премудростях системы типов, но я на нём просто не могу писать (и читать чужой код еле могу, см. выше). А на хаскеле — могу, быстро и эффективно. Хорошо, что есть много языков.
(Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
вот, например, сигнатура, подобных которым довольно много встречается. И что делает такая функция? Нет, мы знаем, конечно, что это foldM
, но такого рода функций понаписать можно бесконечное количество (можно через Йонеду посчитать). Плюс в зависимости от конкретных m
и t
поведение будет различное.При этом, программисты на Haskell верят, что по типу можно понять семантику, и комментарии пишут редко (вот в Lisp такой установки нет, и комментариев там полно в коде). А печальнее всего то, что в итоге выясняется: такого рода абстрактная функция применяется только один раз, и только к одним конкретным экземплярам
Monad
и Foldable
, и только тогда становится понятной идея автора. Для выяснения этого факта приходится несколько часов вызывать ошибки компиляции при помощи дырок, чтобы разобраться в коде. Несколько часов — это слишком долго для того, чтобы прочитать код и понять, что же он делает.В Lisp с этим намного проще: можно в REPL на живой системе запустить функцию и посмотреть, от чего она работает и от чего падает. В Haskell, как бы, почти то же самое можно сделать, но если функцию кормить правильными типами, она будет выдавать нечто, и понять из этого нечто, чего же там на самом деле задумано, особенно, если монада какая экзотическая и самопальная (и не редко и не монада даже, а просто штука с
>>=
), ну, очень тяжело. Преждевременное абстрагирование — это не очень хорошо.Может, дело в опыте, но я себя заставил решить кучу задач на Haskell в CodinGame и CodeWars, читал и разбирал чужие решения, не помогло.
При этом, программисты на Haskell верят, что по типу можно понять семантику, и комментарии пишут редко (вот в Lisp такой установки нет, и комментариев там полно в коде)
Ну вот вы сами кидали выше ссылку, и я там в phtml-internal
комментарии, конечно, вижу, но скупые и совсем не там, где хотелось бы.
В Lisp с этим намного проще: можно в REPL на живой системе запустить функцию и посмотреть, от чего она работает и от чего падает.
Угу, проще. Особенно если функция с побочными эффектами и зависит от глобального состояния этой самой живой системы. И особенно, если то, что этой функции можно скормить, можно сконструировать лишь при помощи пятка других функций.
Monad m
в ограничениях. И поди ж ты раскопай, что это на самом деле всегда что-нибудь про IO
.И почему все так боятся побочных эффектов? Да, нужны методы их контроля, и в Lisp они есть: можно исполнять функцию в её собственном окружении и наблюдать за изменениями в этом окружении. Но без побочных эффектов, ведь, не обойтись, особенно в системном программировании или в математическом моделировании с большими данными. Вы вот видели код на Haskell, который это делает? Там сплошное IO да IO погоняет, прямые доступы в память и все прочие прелести такого программировать (можно на benchmarks game посмотреть). А нормальных инструментов отладки, в отличии от экосистемы Lisp, в экосистеме Haskell нету, потому что всё это упрятано под IO, а IO для языка абстрактна и формально чиста. Но реальность не чистая, к счастью (не хотелось бы жить в абсолютно детерминированном мире одной функции).
Проблема в том, что у такой процедуры будет какая-нибудь абстрактная сигнатура, у которой будет абстрактная Monad m в ограничениях. И поди ж ты раскопай, что это на самом деле всегда что-нибудь про IO.
Нет, Monad m
там не будет, это слишком слабое ограничение для того, чтобы влиять на глобальное состояние. Там будет какое-нибудь MonadIO m
, или PrimMonad m
, или вообще, не знаю, STM
.
Вы вот видели код на Haskell, который это делает? Там сплошное IO да IO погоняет
Как раз на выходных вспомнил, что давно не писал код, который запускается, и поигрался с ручной реализацией DFT. За время масштаба часа-двух (из которых, наверное, полчаса тупил и думал, как мне соответствующий оператор после того, как я его отсепарировал, выразить не поэлементным умножением, а через трёхмерные тензоры — короче, это функция rowise
в коде ниже) наваял реализацию с repa, а потом ради интереса с accelerate. Найдёте
center :: Acc (Matrix (Complex Float)) -> Acc (Matrix (Complex Float))
center = imap $ \(I2 x y) v -> ifThenElse (even $ x + y) v (negate v)
dotp :: (Shape sh, Num e) => Acc (Array (sh :. Int) e) -> Acc (Array (sh :. Int) e) -> Acc (Array sh e)
dotp a1 a2 = sum $ zipWith (*) a1 a2
rowise :: Num e => Int -> Acc (Matrix e) -> Acc (Matrix e) -> Acc (Matrix e)
rowise resultSize m1 m2 = dotp m1' m2'
where
m1' = replicate (constant $ Any :. All :. resultSize :. All) m1
m2' = replicate (constant $ Any :. resultSize :. All :. All) m2
dft :: Image Pixel8 -> Image PixelRGB8
dft img@Image { .. } = generateImage (\x y -> uncurry3 PixelRGB8 $ rgbsRun `indexArray` (Z :. y :. x)) resultSize resultSize
where
resultSize = imageWidth
dims = Z :. resultSize :. resultSize
imgComplex = compute $ center $ use $ fromFunction dims $ \(_ :. y :. x) -> P.fromIntegral (pixelAt img x y) / 255 :+ 0
exps = generate (constant dims) $ \(I2 x y) -> exp $ mult * fromIntegral (x * y)
mult = constant $ -2 * pi * (0 :+ 1) / P.fromIntegral resultSize
ps = compute $ rowise resultSize exps imgComplex
fs = rowise resultSize exps ps
maxR = the $ maximum $ flatten $ map magnitude fs
logScalingC = 1 / log (1 + maxR)
toInt v = round $ v * 255
rgbs = flip map fs $ \c -> let phase' = 0.5 + phase c / (2 * pi)
logMagn = logScalingC * log (1 + magnitude c)
RGB_ r g b = toRGB $ hsl (phase' * 360) 1 logMagn
in T3 (toInt r) (toInt g) (toInt b)
rgbsRun = run rgbs
хоть одно IO?
А там, между прочим, и параллелизм из коробки, и возможность выполнять эту ерунду хоть на процессоре (с JIT-компиляцией), хоть на видяхе (тоже с JIT-компиляцией) — это исключительно вопрос того, из какого модуля берётся run
в последней строке процитированного исходника.
При этом вычисление одной матрицы с repa работает ровно столько же на одном ядре, сколько вот
int main()
{
std::vector<float> a;
a.resize(1024 * 1024 * 1024);
std::vector<float> b;
b.resize(1024 * 1024 * 1024);
std::iota(a.begin(), a.end(), 10);
std::iota(b.begin(), b.end(), 500);
auto start = high_resolution_clock::now();
double result = 0;
for (int i = 0; i < a.size(); ++i)
result += a[i] * b[i];
auto end = high_resolution_clock::now();
std::cout << result << std::endl;
std::cout << duration_cast<milliseconds>(end - start).count() << std::endl;
}
плюсовый код: для картинки ширины N пикселей для вычисления, например, элемента матрицы ps (размером NxN) требуется сделать N умножений и N — 1 (считайте, N) сложений — то есть, 2N³ действий, как и в этом коде для N = 1024.
А, ну и специально для любителей гомоиконности: не сильно сложно сделать так, чтобы компиляция была не JIT, а AOT, и под CPU (или под видяху) код генерировался не во время выполнения, а во время компиляции самого хаскель-кода. Вопрос использования маленького кусочка template haskell.
прямые доступы в память и все прочие прелести такого программировать
Максимум — условный unsafeIndex
, который без рантайм-проверки границ. А почему? А потому, что система типов слишком слабая (а не слишком сильная), завтипов нет, и доказательство того, что доступ корректный, не то что произвести нельзя, а выразить нельзя.
(можно на benchmarks game посмотреть)
Я бы не стал делать выводы ни о каком языке по benchmarks game, потому что они оптимизируют сильно другую функцию полезности. В реальности так никто не пишет код (вернее, писать такой код сильно неоптимально) что на хаскеле, что на плюсах, что на go.
По крайней мере, я регулярно развлекаюсь написанием всякой ерунды на хаскеле и попытками её оптимизировать, и лезть в IO там не нужно ну вот вообще никогда было. Максимум — ST
, да и то редко и изолированно.
хоть одно IO?
$ git clone https://github.com/AccelerateHS
/accelerate
...
$ cd accelerate/
$ grep unsafePerformIO * -R | wc -l
48
$ grep indexByteArray * -R | wc -l
37
Accelerate — это красивый интерфейс для низкоуровневого интерпретатора accelerate-кода.
Максимум — условный unsafeIndex, который без рантайм-проверки границ. А почему? А потому, что система типов слишком слабая (а не слишком сильная), завтипов нет, и доказательство того, что доступ корректный, не то что произвести нельзя, а выразить нельзя.
В завтипах массивы с произвольным доступом по двоичным индексами не выражаются. Чем сильнее система типов, тем меньше свободы в структуре термов. Индуктивные конструкции — это, ведь, только деревья. Смоделировать массивы и двоичные значения можно, через Pos и Vector. Но это будут только модели. Как их компилировать в эффективный код, не понятно. HoTT до такого уровня переноса доказательств не развилась (да и разовьётся ли вообще? В кубических теориях существенные ограничения, а других для вычислимого автоматического переноса доказательств особо и нет).
Accelerate — это красивый интерфейс для низкоуровневого интерпретатора accelerate-кода.
В конце концов там всегда будет какая-то связь с ОС, а там без IO никуда. Что важно — в том коде, который я пишу, никакого IO нет и не нужно.
В завтипах массивы с произвольным доступом по двоичным индексами не выражаются.
Вполне выражаются.
Индуктивные конструкции — это, ведь, только деревья. Смоделировать массивы и двоичные значения можно, через Pos и Vector. Но это будут только модели. Как их компилировать в эффективный код, не понятно.
Компилятор идриса вплоне себе с радостью компилирует классический Nat
, который
data Nat : Type where
Z : Nat
S : Nat -> Nat
в обычную арифметику, без того, чтобы делать O(n)-структуру в памяти для числа n.
Ну и точно так же у вас теперь в каком-нибудь Data.Vector
будет торчать не какой-нибудь unsafeIndex :: Vector a -> Int -> a
, а index :: (v : Vector a) -> (n : Int) -> (n <= length v) -> a
, и последний аргумент там вообще может не использоваться в рантайме, компилятор его с радостью сотрёт, и будет у вас эффективная компиляция.
Нет, мы знаем, конечно, что это foldM, но такого рода функций понаписать можно бесконечное количество (можно через Йонеду посчитать).
Так это неважно: библиотечных функций, которыми пользуются часто, не так много.
Плюс в зависимости от конкретных m и t поведение будет различное.
Оно будет одинаковым на уровне выше (точно так же, как поведение +
одинаковое для всех чисел, хотя 1 + 2 и 3 + 4 немного различаются).
А вообще это можно использовать как аргумент против дженериков или higher-order functions — вон, в этих наших C++ поведение std::sort
тоже отличается в зависимости от того, какой компаратор туда передали.
и не редко и не монада даже, а просто штука с >>=
Мне, наверное, везло, но я не встречал не-монад, реализующих Monad
, вне учебного кода.
Олсо, ждём зависимые типы в хаскеле, чтобы можно было доказать, что ваша монада — монада.
Может, дело в опыте, но я себя заставил решить кучу задач на Haskell в CodinGame и CodeWars, читал и разбирал чужие решения, не помогло.
Ну фиг знает. Я своё погружение в хаскель начал с Real World Haskell, и что-то вот прям как-то зашло.
Оно будет одинаковым на уровне выше (точно так же, как поведение + одинаковое для всех чисел, хотя 1 + 2 и 3 + 4 немного различаются).
Это же не typelevel (затащить можно, конечно), а уровень термов и runtime. Но, допустим.
Хотя отличия и есть в конечном результате, многие свойства сохраняются, кроме, собственно, свойств самого сложения, как абелевой группы: отношения с порядком, с умножением, способы представления в системах исчисления, чётность или не чётность, простота или не простота и так далее, есть куча изоморфизмов, если это именно числа, и тому подобное. Привязка сложения чисел к контексту реальности широкая.
Но если мне скажут: вот тебе абелева группа, как я смогу понять, что это и о чём это? Это может быть что угодно, от многочленов до векторных пространств различной размерности и поведение вне групповой операции будет разное. Ну, да, я буду знать, что элементы можно свободно переставлять при операции, и где-то есть противоположные элементы, и для каких-то математических алгоритмов только это и важно. Но проблема в том, что только этих математических алгоритмов не достаточно. Данные из моей абстрактной группы должны быть связаны с реальностью, и результат вычислений может радикально отличаться по семантике (группа Z_5 или группа параллельных переносов рассказывают совсем разные истории). Без вписывания в некоторый глобальный контекст решаемой задачи абстрактный код не понять.
Возможно, это лично моя проблема, и опытный Haskell-ист сразу видит идею, заложенную в чужой абстрактный код. Но проблема в том, где взять ресурс, чтобы стать опытным Haskell-истом?
В Си++ та же самая проблема, на самом деле. Только не с sort, а с шаблонами. Она отчасти смягчается тем, что в Си++ культура другая: там не пытаются все идеи упаковать в минимальные интерфейсы, наоборот всё многословно и интерфейсы богатые. И, вроде как, по структуре этих интерфейсов можно примерно прикинуть, о чём идёт речь и как оно работает.
Олсо, ждём зависимые типы в хаскеле, чтобы можно было доказать, что ваша монада — монада.
Наверное, я всё же напишу отдельную статью об этом. Я копался в такого рода доказательствах. Даже у Microsoft с огромным коллективном исследователей и бесконечными ресурсами ушло 1.5 года и сотни тысяч строк кода на доказательство одной криптографической фитюльки, доказательство которой в учебнике занимает несколько страниц почти очевидного текста.
Зависимые типы, как средство доказательства корректности, — не технология, учитывая те объёмы кода, которые требуются в production для решения задач, а просто любопытная математическая концепция. А на примере какой-то доказанной FS для Linux видно, что подход «докажем для ключевых кусков корректность, а для остального не важно», не работает. Ну, доказали они корректность FS в своей системе аксиом, а потом оказалось, что эта система не отражает реальную работу Linux, и чего делать?
А, ведь, есть ещё проблемы с параллельностью, где без сессионных типов и проверки моделей не обойтись. И что будем делать? Если у тебя нет суперкомпьютера и недель свободного времени, то ты и не можешь писать параллельные программы? Ну… Хорошая, конечно, концепция для защиты больших корпораций от особо активных disrupt-граждан…
Хотя отличия и есть в конечном результате, многие свойства сохраняются, кроме, собственно, свойств самого сложения, как абелевой группы: отношения с порядком, с умножением, способы представления в системах исчисления, чётность или не чётность, простота или не простота и так далее, есть куча изоморфизмов, если это именно числа, и тому подобное. Привязка сложения чисел к контексту реальности широкая.
Ну так и в случае кода на типах вы либо пишете что-то очень абстрактное, где вам плевать на конкретную монаду (mapM
, foldM
, whenM
и так далее), либо вы пишете что-то менее абстрактное, но тогда у вас там будет не Monad m
, а, не знаю, MonadError String m
, и тогда у вас куда меньше вопросов о поведении конкретной монады.
Возможно, это лично моя проблема, и опытный Haskell-ист сразу видит идею, заложенную в чужой абстрактный код. Но проблема в том, где взять ресурс, чтобы стать опытным Haskell-истом?
А хрен знает. Код писать, как и с другими языками, наверное. Ну и чужой читать, да.
Паршивый код, к слову, на любом языке можно найти. И это тоже полезный опыт — кроме понимания, как надо писать, очень важно понимание, как писать не надо, и эти шишки тоже лучше набивать на чужих лбах, а не на своём.
В Си++ та же самая проблема, на самом деле. Только не с sort, а с шаблонами.
С шаблонами проблема в том, что там, если можно так выразиться, типизации нет, поэтому они эффектно разваливаются в момент использования (просто если момент использования для кода на каком-нибудь питоне — это рантайм, то для плюсовых шаблонов это всё ещё компилтайм, просто в другом месте).
Она отчасти смягчается тем, что в Си++ культура другая: там не пытаются все идеи упаковать в минимальные интерфейсы, наоборот всё многословно и интерфейсы богатые. И, вроде как, по структуре этих интерфейсов можно примерно прикинуть, о чём идёт речь и как оно работает.
Это вы блоги людей за последние лет 10 не читали, включая всяких там Саттеров или Титусов Винтерсов.
Зависимые типы, как средство доказательства корректности, — не технология, учитывая те объёмы кода, которые требуются в production для решения задач, а просто любопытная математическая концепция. А на примере какой-то доказанной FS для Linux видно, что подход «докажем для ключевых кусков корректность, а для остального не важно», не работает. Ну, доказали они корректность FS в своей системе аксиом, а потом оказалось, что эта система не отражает реальную работу Linux, и чего делать?
Давайте всё же не будем сравнивать доказательство корректности ФС с доказательством того, что ваша монада — на самом деле монада? Там-то доказательства смешные.
А статью я как-нибудь сам напишу. Серьёзно, я тут не то что ФС, с которой ничего не понятно, а систему типов, с которой-то всё понятно, уже полгода формализую, и только где-то процентов 30-50 работы сделал. А на бумажке бы было неделя-две (правда, на бумажке я бы ещё не поймал минимум две ошибки, которые я поймал во время формальной верификации на агде, но кого это волнует).
Тем более, что альтернатив-то нет. Тестами тоже можно обложиться, а баги потом всё равно лезут, и лезут довольно много. Верифицированному коду (особенно если он достаточно изолированный — например, блокчейн, а не модель ФС, работающая с моделью линухового ядра, где действительно можно сделать очень много ошибок хотя бы в построении этой самой модели ядра) доверия всегда больше, чем протестированному коду.
А, ведь, есть ещё проблемы с параллельностью, где без сессионных типов и проверки моделей не обойтись. И что будем делать? Если у тебя нет суперкомпьютера и недель свободного времени, то ты и не можешь писать параллельные программы?
Зачем?
Может тогда просто смириться с тем фактом, что баги будут всегда?
По крайней мере я часто слышу, что начиная с ...-ых годов ничего нового не изобрели (кроме завтипов, которые конкретно сейчас мы и обсуждаем)
Тем более, что альтернатив-то нет. Тестами тоже можно обложиться, а баги потом всё равно лезут, и лезут довольно много. Верифицированному коду (особенно если он достаточно изолированный — например, блокчейн, а не модель ФС, работающая с моделью линухового ядра, где действительно можно сделать очень много ошибок хотя бы в построении этой самой модели ядра) доверия всегда больше, чем протестированному коду.
Есть такая быль о верифицированном, но не протестированном коде: «катастрофа Ариан 5». Код был верифицирован, но не протестирован. Когда ракету решили запустить по другой траектории, оказалось, что спецификации неверные, оборудование в соответствии с формальной спецификацией отключилось, а ракета взорвалась.
У NASA тоже есть парочка таких историй, например, о миссии Deep Space 1, в которой возникла ошибка как раз в верифицированном участке кода. Пришлось удалённо патчить систему.
Проблема в том, что код — это физический объект, а не математический. И соответствие чисто математической модели мира самому миру — это вопрос веры. Модель — всего лишь гипотеза, и говорить, что мы доверяем гипотезе без проверки в реальном мире только на том основании, что гипотеза написана на каком-то особом языке, не совсем научно.
Я, пожалуй, действительно, напишу обо всём этом отдельную статью.
P.S. Ну, а альтернатива есть. Так структурировать систему, чтобы можно было отдельно проверять поведение компонент максимально большим числом всевозможных способов. Есть же не только теория типов, есть и другие методы.
Не напомните, на каком языке делалась Ариан 5, и как проверялась корректность самой спеки?
Упомянутые мной баги, если что, нашлись отчасти и потому, что сама спека была неконсистентна.
Проблема в том, что код — это физический объект, а не математический. И соответствие чисто математической модели мира самому миру — это вопрос веры.
Мы точно всё ещё обсуждаем проверку того, что ваш тип — монада?
SPARK Ada. Код проверяли и автоматически, и люди, он сработал по спецификации. Проблема была в самой спецификации, которая говорила: если возникнет переполнение целочисленных значений, отключай оборудование. Переполнение случилось из-за других параметров траектории, автопилот в соответствии со спецификацией и отключил. Все настолько верили в корректность кода, что не потрудились провести копеечное тестирование соответствующего модуля.
Наверное, проблему можно было бы избежать, если бы разработчики потребовали отсутствия переполнений в спецификации. Задним умом, конечно, все сильны. Какая бы техника верификации подсказала бы им это сделать?
На лиспе трудно (невозможно) писать не вникая
Почему? Нет, я согласен, что в лиспе богатый набор языковых возможностей и есть где разгуляться. Но разве на нём нельзя писать как на условном питоне? Да, придётся освоиться с непривычным синтаксисом, но я бы не назвал это "вникать".
Я понимаю, когда вот такое говорят про С/С++: там невнимательность действительно приведёт к паданием, утечкам памяти и прочему UB.
Один из самых солидных репозиториев компонентов на любой вкус у хаскеля, и чё-то как-то на массовость не особо влияет.
Вероятно, это необходимый, но не достаточный критерий. Впрочем, я не сомневаюсь, что не будь у хаскеля библиотек, то популярность его была бы ещё меньше.
Ну и, кстати, по моему личному мнению "осилить" (на базовом уровне) лисп проще чем хаскель.
С репозиторием компонетнов в Haskell как раз все не очень хорошо. Разработчики так не особо заботятся о совместимости и получается что нужные тебе пакеты зависят от несовместимых версий третьего пакета.
Это давно починено в Stackage LTS. Я очень давно не встречал пакета, который бы был нужен и при этом не работал бы с LTS.
Кроме того слышал, что многие пакеты не тестируются и не работают в экзотических системах, типа windows, но искать где это проверить это лень, только с чужих слов знаю.
Да, это так. Windows для хаскеля совсем не приоритетный таргет.
Неплохой еще CCL (Closure Common Lisp), особенно под Mac OS X. Под x86 работает в среднем раза в 2 медленнее SBCL, но тоже вполне шустро. В SBCL есть одна неудобная вещь: нельзя на ходу увеличить heap и стеки, а я много экспериментирую с массивами по несколько гигабайт я тяжелой рекурсией, в результате чего регулярно проваливаешься в дебаггер или вообще вылетаешь. CCL выделяет себе память по мере надобности. Впрочем, гигантский растущий код может так и саму машину повесить. Пожалуй, это единственный плюс. В остальном SBCL лучше всего.
Пробовал ради интереса LispWorks и Allegro Lisp, но не заметил существенных особых достоинств. ECL производит крошечный бинарник и может использоваться как встроенный Лисп в программе на C, однако там глюков в изобилии, компилирует он на два порядка медленней SBCL, буквально в 50-100 раз, и там невозможно по-человечески отраживать код. Хотел, например, сравнить его скорость, а там (time) вообще без каких-то ухищрений не работает.
ABCL - глючный тормоз, требующий Джаву. Когда-то пользовался CMUCL, но это и был тот же SBCL. GCL- просто глюкалово. Впрочем, там и оговорено, что это не ANSI.
А вот Вы знаете, что в SBCL есть даже встроенный ассемблер? Там вообще очень много полезного, но плохо документированного (если вообще), помимо самой оболочки ANSI CL. Например, threads удобно оформлены в пакете sb-thread. Хотя при использовании получается уже не стандартный CL, но именно SBCL, к тому же не всегда портабельный.
https://pvk.ca/Blog/2014/03/15/sbcl-the-ultimate-assembly-code-breadboard/
Вообще лично у меня есть какой-то психологический барьер перед «чисто функциональными» языками типа Lisp и Haskell. Хотя функциональная парадигма, все эти лямбды и замыкания в гибридных языках общего назначения мне очень нравятся, но вот сами «чисто функциональные» языки по прежнему воспринимаю как нечто абстрактное с неким налетом таинственности, непостижимости и элитарности:) А целенаправленно изучать лень, да и нет каких-то реальных задач для этого.
Не сказал бы. По личному опыту написания что компиляторов (прочитал файл в строку и потом мучаешь ее чисто и функционально), что всяких около-REST-сервисов, которые дергаются по сети и ходят в БД — костыли для последних не нужны, а монады — это, считайте, такой проверяемый компилятороом dependency injection. При этом, что удивительно, функциональщина позволяет одновременно, например, иметь некое глобальное состояние и при этом иметь к нему конкурентный доступ, не заморачиваясь с синхронизацией, гонками, дедлоками и тому подобным.
Я немного ковырял пролог когда-то давно, и, если честно, не уверен, что с ним удобнее, чем с доступным для написания трансляторов в хаскеле инструментарием, от построения парсеров до всяких там оптимизаций и форматтеров.
если иметь в виду обычные грамматики — так вот БНФ прямо записывается на прологе — и всё
Это почти так с каким-нибудь megaparsec'ом (разве что, написать эквивалентный BNF часто бывает куда сложнее, чем комбинаторный парсер нафигачить по-быстрому).
пролог и есть это самый инструментарий, всё же логика высказываний, метод резолюции
Как с временем работы и его оценкой?
Есть генераторы парсеров, с помощью которых и так можно генерировать парсеры под любую платформу. К тому же грамматика любого более менее распространенного языка не совсем БНФ — там присутствуют семантические предикаты, а их уже сложнее описывать в рамках логического языка.
Может, дело в грязном форматировании, но я сходу не увидел, где здесь, например, определено identifier
?
А, всё, понял, откуда что берётся и где это в цитате выше.
Блин, как вы там без типов живёте вообще? Получили какую-то штуку, а дальше что с ней делать?
Олсо, для сравнения — тыц и тыц мелкий парсер для самодельного языка, примеры которого здесь. Ну и потом с типами можно делать всякие прикольные вещи, вроде переименования переменной в AST в пару строк.
Так структура выражения — это ж AST, она тоже может быть типизирована.
Там даже есть простор для всяких финтов, когда оказывается, что AST — это функтор и вообще Traversable, например, и валидация получается от компилятора нахаляву (потому что он умеет выводить соответствующие инстансы, и это вопрос вызова одного traverse
)… Но это совсем другая история.
Или, опять же, как выше, можно делать всякие проходы по структуре, основываясь на типах её узлов.
В этом же разделе есть и другие статьи, которые дают более глубокий взгляд на перечисленные аспекты.
Но на самом деле, когда вы говорите о «чисто функциональности» Лиспа, я понимаю, что вы еще не написали на лиспе даже Hello World — и немного завидую — у вас впереди может быть очень много увлекательного. Просто как-нибудь вечером откройте lisper.ru/pcl и если после первого раздела станет интересно — у вас в запасе появится совершенно новый способ думать о программировании
Я бы не назвал лисп чисто функциональным. От хаскеля он вообще максимально далёк.
Каждая новая парадигма пытается остроиться от прошлого этим методом:
— Структурное программирование — это хорошо, а все остальное — неструктурированные программы, в общем, мусор и хаос.
— Грамотное (literate programming) программирование — это хорошо, все остальное — неграмотное (отличная шутка, спасибо мистер Кнут :)
— Функциональное — то же самое, остальное — не функционально, звучит как «не работает»
О, это прекрасный вопрос!
А что такое тогда функциональный язык? Где функции — граждане первого класса, как говорится, и их можно передавать в другие функции и возвращать оттуда? Так тогда любой современный язык функциональный, кроме, наверное, С.
ИМХО функциональщина — это про контроль эффектов этих функций, где и пригождаются типы.
Может просто книжки хорошей не попадалось? Посмотрите "Структура и интерпретация компьютерных программ", буквально пары-тройки первых глав должно хватить.
А в чём проблема? Не понятно. Lisp — это процедурный язык. Можно писать в стиле Вирта. Да и функция вставки элемента в дерево, которая из прежнего дерева формирует новое, не выглядит особо сложной. Она отличаться будет от версии в стиле Вирта только тем, что вместо изменения узлов дерева будут формироваться новые узлы, содержащие указатели на поддеревья, из которых один будет указывать на изменённое поддерево с новым значением. В Prolog значения так же строятся, ведь.
Фактически Форт и Пролог — для меня больше головоломки, чем языки на которых можно что-то писать.
Я вообще не очень понимаю, как взрослый (вроде бы), вменяемый программист может уклоняться от изучения чего-либо нового — будь то язык, парадигма или технология. Это сюр какой-то…
На Прологе — в университете.
В данный момент ни то ни то мне не надо.
Как упражнения для развития мозга — да, нормально. Как язык для разработки уже средних проектов — не подходит, ибо сложно потом в команду найти хоть помощника джедая, не говоря про второго джедая.
Форт процессоры? Ну за 15 лет ничего особо в этой области не произошло.
Взрослый программист имеет выбор, что изучать. Есть куча всего, что полезно для реальной работы.
> Форт процессоры? Ну за 15 лет ничего особо в этой области не произошло.
Ну да, ну да… www.greenarraychips.com/home/products/index.html
> Есть куча всего, что полезно для реальной работы.
— Это например? В 2010-ом мне то же самое говорили про кучу вещей, например PHP и Smarty — и где теперь это все? А Common Lisp, Forth, Smalltalk, Prolog — живут и до сих пор вдохновляют программистов, новые проекты и решения в составе старых проектов. Потому что «хорошую идею нельзя убить» (с)
Ну сравните количество выпущенных форт-машин с другими микроконтролерами. А так то на современной базе можно и АДА-машину вместе со встроенным компилятором сделать. Не удивлюсь, если такая есть.
smarty ну уж точно не мертвее проектов на Форте.
Мне не нужно количество выпущенных микроконтроллеров. Мне нужно выбрать камень под задачу. Фортовый камень заруливает всех — посмотрите характеристики под те задачи, под которые он сделан
Это как в плюсах фигурная резьба по массивам, мешанина операторов или там математика указателей. Иногда надо, но в реальной жизни всё это оборачивается во всякие for-each, смартпойнтеры и т.п. И не выплывает. Так же и в Лиспе — очень часто вы не вызываете руками сами себя, а даёте эту работу fold'ам, map'ам и прочему стандартному синт. сахару, программа выглядит вполне понятным списком задач рантайму.
Ну и со временем, если надо, стандартный вид такой рекурсивной функции в голове прописывается — об него не спотыкаешься, а сразу понимаешь, что здесь происходит.
Могу предполжить, что если бы я был вами, то я бы написал надстройку над лиспом с элементами Ады, если есть многопоточность и Пролога — и это все еще оставался бы лисп. Ну а если надо вызвать числодробилку, и нужна большая производительность чем дает компилятор SBCL, то можно вызвать сишный код через FFI.
Это реально швейцарский нож, со способностью трансформации под задачу. Больше не надо выбирать язык под проект, в процессе работы над проектом язык станет таким, как нужно.
Мне приходится заниматься примерно тем же. Lisp со всем этим справляется. Мне нравится, потому что теперь мне достаточно Lisp и Си для разнообразных задач, не нужно забивать голову другими языками, можно забивать её математикой и алгоритмами. Для Lisp есть, кстати, prolog-библиотеки.
Люди же часто возвращают объекты, чтобы возвращать объекты, и никого это не смущает. Функции похожи на простые объекты в некотором смысле: структура с окружением (значениями переменных) и указатель на код, который можно вызывать с дополнительными аргументами (считайте метод call). Весьма удобно, потому что часто объектов с одним методом хватает для реализации алгоритмов, а дополнительный код с описанием классов и конструкторов писать не нужно
Сколько подобных языков сейчас? Я знаю только Си.
JavaScript (V8, SpiderMonkey, Chakra, и куча менее популярных)
С Лиспом всё так. Вот только ему нечего предложить в современном мире по сравнению с другими языками. Он уже давно не "элегантное супероружие джедая". Многие языки впитали из него достаточное количество свойств. А те что остались, либо не сильно нужны, либо и вовсе вредны. А та же гомоиконность оказалась переоценённой.
Да это в общем-то уже и не важно. Для массовости не нужно, чтобы язык был идеален. У него скорее не доолжно быть особо слабых сторон, хотя и такое бывает (особенно если учитывать, что не все свойства разные люди оценивают одинаково — потребности ведь тоже разные).
Не очень удачный пример, т.к. он во многом "взлетел" тупо из-за отсутствия альтернативы в своей изначальной нише (в браузере).
Из перечисленного только макросы интересны и они (или другие инструменты кодогенерации) появились в других языках (макросы в Rust, Sweet.js/Babel в JS, и, простите, Lombok в Java).
Рестарты это сомнительная вещь сама по себе. Она запутывает поток выполнения и не известно как будет работать и будет ли вообще в многопоточных приложениях.
MOP переоценён. В любом динамическом языке (Python, Ruby, JS) это вообще не проблема. А в статических обходятся рефлексией и кодогенерацией, если нужно.
Можно ещё было бы вспомнить экзотику вроде комбинаторов методов. Но не будем :)
Ну и да — в других языках есть другие средства кодогерерации, скажем AST трансформации всякие. Понятно, что в силу специфики языка они порой сильно сложнее идеологически, чем лисповые — однако же, это не делает их неприменимыми.
Ну т.е. преимущества у лиспа есть — но видимо таки не решающие.
Ну вообще это естественно — вы же работаете на другом уровне, с другими сущностями.
Ну и насколько я читал (а я только читал), в D синтаксис именно что одинаковый — т.е. я к этому и клоню, что макросы можно сделать в общем-то в любом синтаксисе. Можно сделать в том же, что основной язык, можно в другом. Если синтаксис одинаковый — такие макросы будут лучше.
Если язык согласован
Вот это точно не про Common Lisp. Язык, который появился в результате слияния разных диалектов от разных вендоров. При этом каждый вендор тащил туда свою специфику.
Я могу привести те фичи, которые не перенял ни один язык полностью (хотя тут речь больше про реализации и культуру разработки):
- REPL Driven Development. Сам подход разработки через REPL используется ограниченно (это либо полное написание программы в REPL или когда ты по мере написания кода в редакторе посылаешь куски кода в REPL, который работает на фоне и видишь сразу же результат вычисления). Из мейнстрима наверное ближайший к такому подходу Ruby, из немейнстрима языки семейства ML (SML, Ocaml, Haskell)
- Хотрелоад. Можно изменять код по мере его работы через REPL, не перезапуская ни разу программу полностью. Erlang любят за подобный функционал, но он очень далёк от удобства того, что дают лиспы (могу ошибаться, с Erlang очень поверхностно знаком).
- Компиляция в бинарник с рантайм системой и дебаггером. Когда какая-нибудь Java требует установки рантайма в системе, чтобы запускать программы на Java, программы на Common Lisp, собранные при помощи SBCL, дают эту рантайм систему в конечном бинаре. Из минусов всё это дело весит достаточно много и поставляется каждый раз, из плюсов смотреть предыдущий пункт.
- Макросы даже сейчас далеко не в каждом языке есть. Если они есть, то зачастую весьма ограниченные.
- Агностицизм стиля программирования. Вытекает из предыдущего пункта с макросами, так как язык можно расширять как вздумается. Потенциально лисп может быть приспособлен к любой парадигме без значительных сложностей. На данный момент можно писать императивно как в си (можно хоть вставки на ассемблере использовать), функционально как в каком-нибудь Haskell и в объектно-ориентированном стиле как в какой-нибудь Java. Сейчас популярно «каждый язык для своей задачи», который силён в чем-то одном, лиспы же помогают выражать свои мысли вне зависимости от задачи, хотя это не освобождает от учёта специфики используемой реализации
- Рестарты. В CL есть система рестартов, которая позволяет в рантайме исправить возникающую ошибку. По неизвестной мне причине в мейнстримовых языках такого нет.
Если рассматривать язык в отрыве от реализаций и культуры разработки, то в лиспах как в наборе из текста действительно ничего примечательного нет, а скобки могут только затруднять чтение кода. Другие языки в основном впитали именно языковые особенности, а не особенности реализаций и культуры.
REPL Driven Development.Erlang/Elixir. Но это подход из далеких 80-х и Смолтолка вполне вытеснился «пишу тест, запускаю тест». REPL DD это архаика.
Хотрелоад.даже в Erlang, если вы не пишете для железки, никто практически этим не пользуется. Эра кубернетеса и контейнеров делает это все ненужным. Работаю с Эрлангом последние 7 лет если что.
Макросы даже сейчас далеко не в каждом языке есть.макросы это один из видов решения конкретной проблемы. Не единственная. Макросы за пределами Лиспа очень трудны в освоении и очень сомнительная фича.
Перенимать всё же стоит полезные и уместные фичи.
- REPL Driven Development выбивается из современной практики ведения истории кода в VCS. Дампы состояния интерпретатора (ну или лисп-машины) на роль версий не подходят никак. А уж про коллективную разработку и речи нет.
- Хотрелоад есть много где. Erlang, JRebel для Java, Spring gem в Ruby on Rails, Webpack dev server для JS-а.
- Компиляция в бинарник с рантаймом? Это скорее похоже на выдачу единственного что лиспы умеют за преимущество. А вот нормального tree shaking не завезли. Но если уж так хочется всё запаковать, то тот же докер-контейнер с любым интерпретатором вполне на эту роль подойдёт.
- Про макросы я уже написал выше, там где они нужны, они есть (Rust, Scala), а в динамических языках и без них обходятся. Даже в Clojure макросы хоть и есть, их используют крайне редко.
- Разные парадигмы в одном языке. Разве этим хоть кого-то можно удивить? Все более-менее популярные языки такие. Если же взять избитый лисперами пример про Prolog внутри лиспа, то вот пожалуйста — http://minikanren.org/ реализации на любой вкус.
- Про рестарты тоже уже написал выше. Они необычные, но вот полезные ли? То что они делают достигается простой передачей замыкания.
— Нигде ктоме Common Lisp хот-релоад не реализован на основе _любого_ куска кода — везде надо выгружать помодульно и тщательно следить за состоянием. В CL это просто прозрачно и очень удобно
— Видел много реализаций tree shaking и даже несложно написать свою. Впрочем, сам пока не делал, может есть грабли.
— У нас разные мнения относительно макросов, не будем спорить
— Рестарты очень полезные — они увеличивают выразительность и упрощают понимание кода.
Никаких. Если вы откроете один из моих постов, вы увидите там Scala REPL в виде Spark Shell. Ну т.е. тут все просто — REPL давно не только в лиспе.
>Нигде ктоме Common Lisp хот-релоад не реализован на основе _любого_ куска кода — везде надо выгружать помодульно и тщательно следить за состоянием.
Ну мы это обсуждали вроде. Это хорошо — но это же потенциально и опасно. Зарелоадив произвольный кусок кода произвольным образом можно не только сделать хорошо, но и сломать тоже неплохо. Поэтому если у этого средства нет хотя бы Undo — оно рисковано для прома.
Ну в общем, тут скорее стоит вопрос полезности всех этих фич, в том числе в сравнении с иногда другими фичами других языков, которые решают те же задачи.
Такого REPL как в лиспе я нигде не видел, даже Clojure в этом плане на шаг позади. Больше всего кайфовал на отладке багов где без потери стейта системы можно поменять функцию (добавить отладку и т.п.) и заново прогнать вызов и тут же поправить
В этом вся соль в Lisp можно менять что угодно до\после\вместо функции
В жабке это прибито гвоздями, ну и будем честны REPL в массы в Java не пошел
БОльшинство команд, с которыми мне приходилось общаться, предпочитают классику — пошаговую отладку в Idea
И потерять весь стейт и это без учета всяких Spring boot'ов и других фреймворков которые накладывают свои ограничения на этот процесс
В мире Java ближе всего к CL REPL подошел Clojure но и тот недотягивает
Ну, это очень хороший вопрос, на него боюсь нет простого ответа. Ну вот смотрите, забъем на spring boot, аннотации, и т.п. магию, которая там есть.
Возьмем простой старый спринг, с xml конфигами для контекстов. Чисто для простоты. Вот вы перезагрузили этот конфиг. Понятно, что все дерево контекста нужно будет перестроить, вообще говоря. Каждый объект при инициализации может что-то сделать, например, загрузить в память некие ресурсы, или скажем установить соединение с базой данных, и даже достать оттуда что-нибудь. Ну или из сети что-то запросить. Так вот, проблема при работе со спрингом из отладчика в том, что я могу перекомпилировать класс, но я не могу вот эти вот все вещи, которые были сделаны при старте приложения, просто так из отладчика повторить. А если могу — это де-факто эквивалентно рестарту приложения, и этот рестарт будет безопаснее.
Вот вы когда из лиспового REPL что-то делаете — вы уверены, что ваши изменения вот это вот все учитывают, а не просто работают потому, что в лиспе этих сложностей просто нет? Ну так, чисто условный пример — у вас есть приложение, оно работает. У него заполнен данными какой-то кеш. И вы решили из REPL поменять структуру той сущности, что лежит в кеше. В итоге, то что там уже лежало, осталось старой структуры. А новое стало иметь новую. Вопрос — кто это изменение учтет? В случае Java в кеше скорее всего лежал бы экземпляр класса. И при попытке поменять его сигнатуру, т.е. добавить поля, скажем, мне бы дали по рукам. И мне отчего-то совсем не кажется, что такое поведение неразумное.
В какой-то степени — еще к тому, что у нас статически типизированный язык — т.е. если вы поменяли тип — то что делать с тем кодом, который ожидает старого типа? Он ведь может и не скомпилироваться.
Не давать менять код из отладки таким способом — это в общем-то и есть текущее решение принятое в JVM.
он будет автоматически перекомпилирован в момент следующего обращения (в CL)
У меня наоборот есть вопросы к Java — допустим, у меня есть класс, и на работающей системе есть сотня объектов этого класса.
Если я добавлю в класс поле или пару методов — что мешает Java обновить объекты этого класса при следующем обращении к ним?
Надо как-то конкретнее. У вас был метод в классе, вы его выкинули. Он использовался где-то (как рантайм найдет те места, где он использовался — это отдельная история, но неважно). Но каким образом предлагается автоматически обновить те места, где он использовался?
Ну т.е. тут технически рантайм просто не может сам ничего перекомпилировать. Все сценарии hot reload выглядят примерно так — есть исходники где-то в другом месте, мы подключаемся отладчиком, перекомпилируем кусок кода (тут у нас в наличии компилятор с нужного языка или языков), и подсовываем JVM через механизм отладки новую версию байткода.
Причем вполне реальный сценарий в моей практике, когда этих JVM вообще много, и к некоторым у нас просто нет доступа. Т.е. грубо говоря, заменив байткод в одной из них, мы просто получим ошибки, когда они начнут общаться друг с другом через сеть.
Поэтому все эти плюшки лиспа — это конечно хорошо, но я могу не вставая с места вообразить еще несколько сценариев, когда у меня бы такое было невозможно.
Так что возможность наверное все-таки есть. И Datomic в общем совсем не игрушка. Причем есть ведь еще и такая интересная фиговина.
Действительно если у нас нетривиальное обновление\изменение\инвалидация кэша в зависимости от окружения\запросов и т.д. то заново загрузить его под новую структуру\класс в Lisp мы не сможем. Тут у нас с вами паритет
Но если посмотреть на подходы к разработке в Java и CL они в корне отличаются
На Java бОльшая часть разработки опирается на помощь Idea, автокомплит, типы и т.д. Т.е я не запускаю приложение из раза в раз, потому что это достаточно медленно
В Lisp же наоборот я начинаю с "запуска приложения" и постоянно в нем что-то дорабатываю, добавляю поля в структуры, меняю порядок вызова функций и все это на рабочем приложении
Ну и я собственно пытался привлечь внимание к тому, что не всегда можно так легко «перезагрузить» код в работающем приложении. Это я еще Spark не упоминал, где это по факту вообще невозможно — и вовсе не потому, что JVM не позволяет.
А как вы расхождение типов фиксите? С лиспом понятно он динамический, а вот с java и scala не понятно
Сравнения, рассуждения, мнения, аналогии, умозаключения. Ничего про сам Lisp. По крайней мере для меня это было, мягко говоря «неожиданно».
Задавая себе после прочтения вопрос «Какие новые знания о предмете я почерпнул из статьи», я с сожалением отвечаю сам себе: «никаких». А казалось бы, какой шанс! У тебя есть статья, в которой ты, своими руками, можешь немножко повлиять на популярность языка, рассказав о нем что-нибудь интересное. Но нет.
//если что, это камень в огород автора, но и переводить такую статью с сомнительной полезной нагрузкой, кажется неоднозначным решением.
github.com/carld/micro-lisp
Такая наивная вера в способности свободного рынка продвигать добро и подавлять зло, в лучшем случае смехотворна, но на самом деле очень печальна, потому что приводит к некоторым очень неправильным предположениям и некоторым очень неправильным решениям.
Это надо отливать в бронзе.
К «самому непереносимому языку в мире»? Ещё как относится! Вам мало того, что придётся писать отсутствующие библиотеки, так ещё и адаптировать их под веер реализаций Scheme.
Когда Если выйдет R8RS (aka Scheme Big), то может семейство Scheme и станет лучше для промышленного программирования. Но R6RS внёс такой раздор в сообществе Scheme, что это маловерятно. У Scheme, похоже, получилось «избежать успеха любой ценой».
Впрочем, отдельные реализации вполне юзабельны. Но библиотек для каждой отдельной значительно меньше, чем можно найти в централизованных репозиториях других языков.
Что не так с Лиспом?