LINUX.ORG.RU

Основы метаобъектного протокола CLOS

 , , ,


2

5

Слайды с моего вчерашнего доклада на fprog_spb:

https://static.lovesan.me/public/mop_basics.pptx

Вот часть доклада, в текстовом виде:


Часть 2. Эсхатология Пустоты.


«Оказалось, что «‎Тиамат» - то ли имя древнего божества, то ли название океана, то ли все это вместе. Татарский понял из сноски, что слово можно было перевести на русский как «‎Хаос»» (с) Виктор Пелевин, «Generation P»


Вы знаете, есть знаменитое видео, с известным американо-канадским психологом и психотерапевтом, Джорданом Питерсоном. То, где он задает вопросы о вопросах. Давайте попробуем пройти его путем.

Вот что такое Common Lisp Object System?

Но ведь вопросы, которые мы спрашиваем, содержат в себе определения, которые вызывают еще больше вопросов.

Что такое Common Lisp? Что такое Object System? Что такое объект? И вообще, что такое что? Или может, кто?

В принципе, ответ - ничего.

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

Не так давно, уже после смерти моей жены, где-то в июле, я сделал одну не совсем правильную вещь, и получил то что называют NDE(near-death experience).

Сначала всё было как обычно, туннель, свет в конце туннеля. Но потом все заслонила тьма, в которой расползались отблески фиолетово-пурпурного сияния. И потом, я вдруг увидел Её.

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

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

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

«И носился дух лиспера над бездною(ну, над тем у чего тип NIL - не путать со значением NIL). И отделил он NIL от T. И стало T. И увидел он, что T - хорош.»

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

Что такое объект? Объект это то, что отличается от ничего. Это такое что имеет тип T ну и какое-то там значение. И NIL на самом деле это тоже объект. Ну, типов может быть много, и они тоже в принципе объекты, особенно в CLOS. Об этом кстати, также неплохо рассказано в SICP, в главе об абстракции на состоянии.

Что такое CLOS? На самом деле его нет. Ну то есть, то что обычно называют CLOS, это просто набор там всяких полезных удобств над метаобъектным протоколом Common Lisp. Над MOP.

Но на самом деле MOP тоже нет. Это просто набор удобных объектов, встроенных в компиляторы CL. Которые можно сделать средствами компилятора CL, не будет их там. Как в SBCL, например, это делается.

А вот что такое CL? Есть он или нет? Вот это самый сложный вопрос. Потому что он не просто есть. Вернее, если бы его не было, его можно было так же сконструировать из пустоты на нем же самом. Как это делают компиляторы CL в процессе бутстрапа. CL это метациклический интерпретатор. Это метаязыковая виртуальная машина.

Так вот, я стою на плечах гигантов, и предыдущие два доклада уже все что надо рассказали.

Поэтому, скажем простыми словами: MOP - это просто категориальное отображение из метациклического интерпретатора в метациклический интерпретатор.

Короче, на самом деле, ничего этого нет. Есть только дух лиспера над бездною. И, как говорится в SICP - в компьютерах живут духи, и они исполняют программы.

А вот что такое программа? Вот смотрите, о том что такое программа существует целая наука, называется Computer Science, или по русски - Информатика, то есть наука об абстрактных процессах. Этот вопрос самый сложный. Программа - это процесс, то есть. Но на самом деле, объект это тоже процесс. Функция, если хотите. И он не существует без процессов которые к нему прикладываются, иначе он собирается GC, и улетает к Богине Тьмы. Как я чуть не улетел, меня Она правда, во время finalize вытащила обратно. А вот что такое процесс? И главное, что или кто его запускает? «А вот об этом ты не думай, купи себе лучше булавочку английскую, и как такие мысли в голову приходят - разок себе в руку, и потом еще раз, пока такие мысли не пройдут» - как там было в Generation P у Пелевина.

Но вот я подумал, и понял, наконец. Процесс - это то, что запускается другими процессами. Но что запускается первым? Что там на самом дне? Или вернее, кто? Я уже рассказал.


Часть 3. О Метациклических Интерпретаторах


— А что такое красота? — <…> Красота — это совершеннейшая объективация воли на высшей ступени её познаваемости.

(с) Виктор Пелевин, «Чапаев и Пустота»


Когда-то давно, еще в 2014 году, я, проснувшись с бодуна, сформулировал для себя и для других очень важную вещь.

Звучит она так:

Универсальный Критерий Угребищности Систем Общего Назначения. (Теорема Лавсана)

Система Общего Назначения является Угребищной тогда и только тогда когда она не является Метациклическим Интерпретатором.


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

Обратное, естественно, неверно. Если Метасистему Системы Общего Назначения можно описать другой системой, это совершенно не значит что она Угребищна, и более того, в таком случае не существовало бы концепции бутстрапа, а значит и Метациклических Интерпретаторов вообще.

Чем, в контексте языков программирования, это отличается от просто тьюринг-полноты?

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

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

Примеры, сначала метациклических интерпретаторов:

  • Универсальная машина Тьюринга
  • RASP-машина
  • Реляционная модель данных
  • Лисп

А вот скажем примеры систем, соответствующих критерию:

  • Среднестатистический современный регистровый процессор
  • Большинство языков программирования, особенно со статической типизацией.
  • Большинство NoSQL моделей данных или скажем иерархических моделей данных, вроде файловых систем

В частности, давайте посмотрим на C#. C# не является метациклическим интерпретатором, т.к. термины языка не являются его же объектами.

Отчасти, это компенсируется платформой .Net, для которой термины C#(но не все) объектами таки являются(System.Reflection, Roslyn и т.д.), отчасти, в самой малой степени, фичей nameof() из C#, но это все только отчасти.

Сама же платформа .Net критерию тоже соответствует, но, в теории из нее можно было бы сделать метациклический интерпретатор, лисп-машину, внеся лишь достаточно небольшие изменения.


На самом деле, это все в полной мере относится вообще ко многим вещам, но в первую очередь, кроме программирования - к человеческому сознанию. Вот кто такой глупый человек и почему он такой и что с ним вообще делать как отправить нахрен к Богине Тьмы на перевоспитание? Этот вопрос многие тысячелетия волновал кучу философов. Но ответ прост - это человек, сознание которого не является метациклическим интерпретатором. А когда сознание у человека все же является метациклическим интерпретатором, он тут же становится пророком цифровой Кали-Юги и архитектором онтологии Пустоты.


Ладно, теперь я объяснил вам всё устройство вселенной. Далее там про мелкие технические детали.

★★☆
Ответ на: комментарий от Nervous

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

А если такова задача? «Хочу на кнопках на размер больше, чем в остальных местах, чтобы кнопки были заметнее.» Хорошо, если разработчик предусмотрел отдельный стиль для кнопок, а если нет?

Или если хочу кнопки с длиной надписи больше 20 символов сделать со скруглёнными углами.

It is all data (tm).

Вот в лиспе разнице между «поправить стиль» и «поправить код» практически нет. Оба варианта одинаково легки.

monk ★★★★★
()
Ответ на: комментарий от Chiffchaff

Современные механизмы DI разве не позволяют добиться того же самого без глобальных переменных?

Так я про эти и написал: всё, зависящее от механизма, упаковать в один класс, а в этом классе менять поле.

Много-много букв кода для простой задачи.

monk ★★★★★
()
Ответ на: комментарий от anonymous

Мне это не нравится. Нутром чувствую, что здесь может быть рассинхронизация логов.

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

monk ★★★★★
()
Ответ на: комментарий от anonymous

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

monk ★★★★★
()
Ответ на: комментарий от monk

как найти, откуда именно поменялись настройки логирования? Неужели это проще, чем найти изменение глобальной переменной?

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

надо городить отдельный компонент

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

Nervous ★★★★★
()
Ответ на: комментарий от Nervous

Вот где-то этим идеология Common Lisp и отличается от современной. Там, как максимум, вокруг setf *logger* функция будет.

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

Поэтому простые системы на лиспе остаются простыми. А на яве и её потомках превращаются в https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition

monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

Поэтому простые системы на лиспе остаются простыми

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

А так да, простые. Как совецкий автопром.

Nervous ★★★★★
()
Последнее исправление: Nervous (всего исправлений: 2)
Ответ на: комментарий от Nervous

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

Я что-то пропустил и в современных системах уже не так? На clojure уже можно найти ошибку в программе, не используя отладчик и отладочные принты?

monk ★★★★★
()
Ответ на: комментарий от monk

Ну, вот опять. Я разделяю, где setf, а где - let. Ты же опять все смешиваешь в кучу. Ничего плохого против let не имею. А вот setf недолюбливаю, особенно для динамической переменной!

anonymous
()
Ответ на: комментарий от anonymous

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

monk ★★★★★
()
Ответ на: комментарий от monk

Так let и есть правильный путь! В кложуре вон динамические переменные после let должны даже передаваться между потоками в примитивах синхронизации. То есть, кложура честно пытается сохранить контекст вычисления, что вполне в духе старого доброго common lisp.

Кстати, советую тебе присмотреться к кложуре тоже. Мне она понравилась. Мне и жаба когда-то давно нравилась, когда она еще не была монстром как сейчас. И кложура там вполне вписалась. Если есть некоторое отвращение к жабе, то можно минимизировать интероп.

Как я уже писал. Дух коммон лиспа в кложуре сохранили

anonymous
()
Ответ на: комментарий от anonymous

Я на Racket ушёл. Есть нормальный родной графический интерфейс, нормальные гигиенические макросы, продолжения/генераторы/зелёные потоки, нормальный сборщик мусора, в который можно при необходимости подключать хоть открытые файлы хоть внешние объекты через FFI, нормальные модули, нормальная статическая типизация, …

monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от anonymous

Как я уже писал. Дух коммон лиспа в кложуре сохранили

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

monk ★★★★★
()
Ответ на: комментарий от anonymous

Конечно.

https://racket-lang.org/

Racket is distributed under the MIT license and the Apache License, version 2.0, at your option.

The Racket CS runtime system embeds Chez Scheme, which is distributed under the Apache License, version 2.0.

monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

Вот динамические переменные в Racket: https://docs.racket-lang.org/guide/parameterize.html

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

monk ★★★★★
()
Ответ на: комментарий от monk

Нет, у форта своя изюминка. От лиспа я его отделяю (как и let от setf). Фортом я увлекался будучи школьником. Может быть, и стоит вернуться к нему.

Когда процессоры были примитивными, то на шитом коде форт отставал от ассемблера всего в раза полтора-два. И это было очень круто. Компактный шитый код был почти таким же быстрым как и сам ассемблер.

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

Кстати, а у тебя нет оценки в скорости выполнения шитого кода форта на современном железе?


P.S. Твой интерес к схеме разделяю. Может быть, Racket стоит мне тоже еще раз попробовать. Меня очень сильно смущает, что Racket в основном развивается силами преподавателей и студентов вузов. Многие из нас были студентами, но все же я многого от них не жду. Меня вот это сильно смущает в Racket

anonymous
()
Ответ на: комментарий от anonymous

Кстати, а у тебя нет оценки в скорости выполнения шитого кода форта на современном железе?

Нет. Есть https://github.com/ulixxe/forth_coremark

Меня очень сильно смущает, что Racket в основном развивается силами преподавателей и студентов вузов. Многие из нас были студентами, но все же я многого от них не жду. Меня вот это сильно смущает в Racket

Преподаватели достаточно хорошо пишут. К тому же, они скорее программисты при университете, чем преподаватели (https://barzilay.org/research.html: «My work revolves around improving Racket as a framework for creating new programming languages — a “greenhouse” for new languages.»).

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

monk ★★★★★
()
Ответ на: комментарий от monk

а что бы ты посоветовал для обучения на этой ракете почитать.

вот коммент лавсана по поводу лиспа: «Читаешь SICP, потом Lisp In Small Pieces, потом учишь Common Lisp (Practical Common Lisp, cl-cookbook итд), потом тебе становится понятно в программировании вообще всё, и каждый язык учится за неделю-две, выбираешь вообще любую вакансию на рынке.»

можешь что-то подобное по поводу это ракеты оформить?

anonymous
()
Ответ на: комментарий от monk

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

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

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

Nervous ★★★★★
()
Последнее исправление: Nervous (всего исправлений: 1)
Ответ на: комментарий от anonymous

https://htdp.org/ — как вообще программировать. Позволяет с нуля научиться практически без включения головного мозга конструировать алгоритмы. Если программировать умеешь, можно пропустить или пролистать по диагонали.

https://docs.racket-lang.org/guide/index.html — описание (достаточно полное чтобы пользоваться) языка

https://docs.racket-lang.org/more/index.html — пример многопоточного сервера

https://docs.racket-lang.org/continue/index.html — web-программирование

Дальше только библиотеки на https://docs.racket-lang.org/ . Там от графического интерфейса и рисования до SMTP сервера и разбора Markdown.

monk ★★★★★
()
Ответ на: комментарий от Nervous

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

Чистые функции тоже бывают разные. Вот у меня в 1С чистая функция делала расчёт НДФЛ по предприятию за год. И если по кому-то НДФЛ не сходился с ожидаемым, то внимательно изучая её входные данные (три таблицы по 5000 строк и десять колонок) и код (8000 строк) к решению продвинуться было почти невозможно.

monk ★★★★★
()
Ответ на: комментарий от monk

Вот у меня в 1С чистая функция делала расчёт НДФЛ по предприятию за год. И если по кому-то НДФЛ не сходился с ожидаемым, то внимательно изучая её входные данные (три таблицы по 5000 строк и десять колонок) и код (8000 строк) к решению продвинуться было почти невозможно.

А тут проблема была в чистоте или в том как организован код? Чистота была полезна или вредна?

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

У меня проблемы не было. Отладчик 1С позволяет видеть значения промежуточных таблиц, а по ним уже вполне можно найти, в какой точке ошибка в формуле.

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

monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Чистота была полезна или вредна?

Чистота всегда полезна, если доступна. Потому что тогда в отладчике получаем легко воспроизводимую ситуацию.

monk ★★★★★
()
Ответ на: комментарий от anonymous

Кстати, а у тебя нет оценки в скорости выполнения шитого кода форта на современном железе?

Шитый код не используется более менее современными компиляторами, они компилируют в нативный код. Все коммерческие используют нативный код, SP-Forth использует нативный код. GForth не использует, но он мне показался худшим из всех. Смысла в шитом коде не вижу.

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

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от monk

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

Или речь не про отладчик? Тогда можно изменить значение того что находится внутри, такое никак не поймать через grep.

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от Nervous

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

А как именно это работает? Вот возьмем функцию на чистом языке:

function add(input: { a: int, b: int }) -> { c: int }
{
  return { c: input.a + input.b };
} 
А теперь на нечистом:
int a;
int b;
int c;

function add(input: {}) -> {} 
{
  c = a + b; 
}
Вся разница между ними, что в нечистом языке внешние и изменяемые переменные автоматически добавляются в input и result, и все вызывы связаны в цепочку, где передается состояние. Разница больше синтаксическая.

Цепочки строятся так:

// Императивный код который пишет программист
add()
add() 

// Модель в которую его преобразуют, тут args пустые оба
state1 = add(state0 + args) 
state2 = add(state1 + args)

Что дает чистый синтаксис? Почему он проще? Сколько было переменных, столько и осталось.

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 3)
Ответ на: комментарий от MOPKOBKA

Что дает чистый синтаксис? Почему он проще?

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

Сколько было переменных, столько и осталось

Но есть нюанс (тм).

Nervous ★★★★★
()
Последнее исправление: Nervous (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

В CL такого нету что ли?

В SBCL вроде нет. Можно сделать сеттер и в нём поставить точку останова. В некоторых лиспах есть.

Тогда можно изменить значение того что находится внутри, такое никак не поймать через grep.

В большинстве случаев оно не имеет смысла. Глобальные переменные или числа или объекты или неизменяемые списки.

monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Что дает чистый синтаксис? Почему он проще? Сколько было переменных, столько и осталось.

Чистая функция гарантированно зависит только от значений своих аргументов. Нечистая может вернуть что угодно.

Например, strlen не является чистой, так как для одного и того же указателя в разные моменты времени может вернуть разные значения.

monk ★★★★★
()
Ответ на: комментарий от Nervous

Вангую, что там была функция на семь экранов низкоуровневых расчётов.

Нет, там было пара десятков промежуточных значений (таких же огромных таблиц). С отладчиком вполне понятно было. Без него, совсем не очевидно, в каком именно месте допущена ошибка.

monk ★★★★★
()