LINUX.ORG.RU
решено ФорумTalks

Какой профит от разделения неймспейсов в CL

 ,


1

3

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

Перемещено tailgunner из development

есть какой-то профит от разделения имен функций и других объектов в плане метапрограммирования

При отсутствии гигиенических макросов легко нарваться на ситуацию:

(DEFMACRO MAKE-FOO (THINGS) `(LIST 'FOO ,THINGS))
(DEFUN FOO (LIST) (MAKE-FOO  (CAR  LIST)))

При раскрытии макроса получаем (DEFUN FOO (LIST) (LIST 'FOO (CAR LIST)))

Если бы LIST как функция и переменная был в одном пространстве имён, то это была бы ошибка.

monk ★★★★★
()

https://www.dreamsongs.com/Separation.html

(Некоторые пропустил)

1. Common Lisp для коммерческих лиспов своего времени - это (спасибо Unununij) как Эсперанто для европейских языков. Язык, который как можно меньше отличается от каждого из них, одновременно избавляющийся от наибольшего количества костылей.

2. Разные неймспейсы лучше читаются если ты редко используешь funcall и function (#'), но хуже, если ты используешь их часто и они превращаются в мусор, занимающий пол кода. Компромисс-с.

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

4. С двумя неймспейсами невозможно без контекста узнать брать у символа значение или функцию. На самом деле профит для метапрограммирования от этого *отрицательный*. «Некоторые программисты придерживаются „основного правила лиспа“ - код наиболее ясный когда требуется минимальное знание контекста, чтобы определить его смысл.» Наличие двух неймспейсов это правило нарушает.

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

6. Как видно по (2), общий неймспейс лучше подходит для функционального кода, который лучше параллелизуется. При разработке стандарта Common Lisp о многопоточности речи не было.

7. Неймспейсов на самом деле не 1 и 2, а 5 и 6 соответственно. Честно, я могу вспомнить только 4 (значение, функция, документация, список свойств), а автор все 6 не перечисляет. Но суть в том, что схлопывание двух из них не так значимо, как может показаться на первый взгляд.

8. Что сказал monk. Без гигиенических макросов общий неймспейс сделает ошибочным кучу кода, который будет работать с отдельными неймспейсами. Причина во ВНЕЗАПНЫХ коллизиях в коде, который писал пять лет назад разработчик, которого давно сбил автобус. С отдельными неймспейсами ВНЕЗАПНЫХ коллизий нет. Если они есть, то они на самом деле закономерные и автору нужно бить по рукам линейкой, чтобы аккуратнее писал макросы.

9. Следствие из (8). Без гигиенических макросов фактически приходится сделать выбор между одним неймспейсом и макросами. Разработчики Common Lisp выбрали в пользу макросов.

10. Экономия памяти. Кажется, что с двумя неймспейсами символ занимает в два раза больше памяти, чем с одним. Конечно, тогда нужно чуть больше символов, но пренебрежимо мало, точно не в два раза. На самом же деле вспоминаем, что неймспейсов на самом деле не 1 и 2, а 5 и 6. «второй» неймспейс прибавляет к размеру символа не 100%, а 20%, которые частично экономятся обратно на меньшем количестве символов. Итоговая разница пренебрежимо мала.

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

12. В Common Lisp есть лексические и специальные (динамические) переменные. Переменные объявленные вне функций всегда специальные, неймспейс функций же глобальный. В Scheme всё то же самое, только т.к. нет своего неймспейса для функций, они все лежат в специальных (динамических), а не глобальных переменных. Т.е. если программист случайно использует переменную с названием, совпадающим с названием уже объявленной функции, то получит специальную переменную вместо лексической. Стоит заметить, что в PLT Scheme => Racket использование специальных переменных сильно затруднили, а глобальные декларации сделали лексическими. Опять компромисс, как и с макросами. Либо удобные специальные переменные, либо один неймспейс.

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

13. Следствие из (1), нужно оставить два неймспейса, т.к. иначе для портирования существующего коа на Common Lisp потребуются слишком значительные изменения кода, и ловля кучи багов из-за коллизий.

14. На момент разработки стандарта один неймспейс был новой идеей, не слишком зарекомендовавшей себя на практике. (Да и сейчас...)

15. Заключение. Есть достаточно серьёзные теоретические доводы в пользу обеих решений, но для Common Lisp главное - сохранить максимум совместимости с существующими коммерческими лиспами, улучшив и упростив там где можно, а упрощать и улучшать ещё можно много где. Поэтому решение что лучше на практике мы оставим дизайнерам будущих лиспов, которые смогут учесть ошибки и Common Lisp, и Scheme.

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

У меня случались трудноотлаживаемые коллизии в Scheme

А можно пример? Хоть один. Или это что-то вроде (define (f list) (list (car list) 2))) ?

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

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

Чем? Ведь везде взаимнооднозначность информации. Если мы используем глобальную функцию в стиле (list 1 2 3), то разницы нет, если локальную let/labels — тоже, если через funcall, то тоже.

Если funcall сделан не примитивом, про который знает компилятор, то (funcall x 1 2) не даст информации о типе x, в отличие от (x 1 2).

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

Неймспейсов на самом деле не 1 и 2, а 5 и 6 соответственно. Честно, я могу вспомнить только 4 (значение, функция, документация, список свойств)

Тогда счётное множество. Каждая пара функции (f x), (setf (f x)), где x — символ вводит новый «неймспейс». На самом деле, всё-таки 1 и 2, так как все остальные не имеют лексической видимости (и могут быть реализованы внешним хэшем).

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

С отдельными неймспейсами ВНЕЗАПНЫХ коллизий нет. Если они есть, то они на самом деле закономерные и автору нужно бить по рукам линейкой, чтобы аккуратнее писал макросы.

Угадай, что не так в макросе

(defmacro debug (x) 
  `(aif (check x) 
        (format t "check ~a = ~a" x it)))

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

Переменные объявленные вне функций всегда специальные, неймспейс функций же глобальный.

Ложь:

[1]> (setf x 1)
1
[2]> x
1
[3]> (defun f () x)
F
[4]> (let ((x 2)) (f))
1
[5]>
функции лексические тоже есть (через flet и labels)

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

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

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

scheme@(guile-user)> (define (f) (list 1 2))
scheme@(guile-user)> (let ((list +)) (f))
$1 = (1 2)
scheme@(guile-user)> (let ((list +)) (list 1 2))
$2 = 3

Стоит заметить, что в PLT Scheme => Racket использование специальных переменных сильно затруднили, а глобальные декларации сделали лексическими.

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

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

функции лексические тоже есть

А это какая функция, не лексическая чтоли: (let ((x 2)) (f))Если бы она динамической была, она бы как раз и связывалась с x = 2

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

С ним может быть столько всего не так, но главная проблема скорее всего в it, которая нарушает заповедь №2 писателя макросов - «Знай откуда берётся каждый символ в твоём макросе». it берётся хз как из недр aif, его не передаёт пользователь аргументом, его не заводит автор макроса debug. Такие переменные в макросах использовать нельзя, но Scheme это не поможет.

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

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

Чем затруднили? Тем, что сделали функциями?

Да.

Всего две лишние скобки, зато точно случайно не перепутаешь.

Всего один #' и funcall, зато случайно не перепутаешь.

Больше отсебятины в «переводе» нет, поэтому с остальными претензиями не ко мне, а к автору статьи, но кажется мне, причина в том, что написана она была в 2001 году и за ПЯТНАДЦАТЬ лет многое (в частности, в Scheme) изменилось.

Gentooshnik ★★★★★
()
Последнее исправление: Gentooshnik (всего исправлений: 1)

Ответ на вопрос ТСа: Профит в CL от разделения пространств имён переменных и функций - в упрощении портирования на него кода с других лиспов того времени, в которых пространства имён тоже были разделены. Можно сказать, легаси.

Вот если задать вопрос в формулировке «в чём вообще преимущества отдельных пространств имён?», тогда можно и срачик развести страниц на десять.

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

А это какая функция, не лексическая чтоли: (let ((x 2)) (f))Если бы она динамической была, она бы как раз и связывалась с x = 2

Нет. Переменная x не динамическая, поэтому и не связывается.

А лексическая функция это

[1]> (flet ((list (x y) (+ x y))) (list 1 2))
3

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

которая нарушает заповедь №2 писателя макросов - «Знай откуда берётся каждый символ в твоём макросе»

Угу. И все новые символы, которые могут быть видимы в раскрытом теле обязательно должны быть введены через gensym. Что фактически запрещает использовать анафорические конструкции в теле раскрытия макроса.

Такие переменные в макросах использовать нельзя, но Scheme это не поможет.

В Scheme этой проблемы нет. it из раскрытия aif и возможный it из раскрытия x будут разными привязками (bindings). Поэтому в Scheme (Racket) макросы по определению гибче (любой макроc CL можно переписать в Racket, но не любой макрос Racket можно переписать в CL).

главный аргумент в пользу Scheme, который инвалидирует все пункты про макросы - в Scheme есть гигиенические макросы.

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

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

Вот если задать вопрос в формулировке «в чём вообще преимущества отдельных пространств имён?», тогда можно и срачик развести страниц на десять.

Почему? Необходимость отдельных пространств имён вытекает из требований

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

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

Вообще, в CL много решений принято с целью упрощения понимания программистами и, видимо, упрощения компилятора: единый образ, в котором происходит и вызов макросов и выполнение программы; AST как список литералов; доступ к любому объекту из любого места (можно создать или изменить символ чужого пакета, даже не экспортированный); интерактивный рестарт на несуществующую переменную.

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

Всего один #' и funcall, зато случайно не перепутаешь.

О! Вот это кстати правильный момент. Несмотря на синтаксическую близость стиль программирования в CL и Scheme радикально различен.

В CL пропагандируется активное использование динамических переменных («это глобальные переменные, но сделанные правильно»), поэтому работа с ними максимально упрощена. Развитый сигнальный протокол и динамические переменные приводят к тому, что в API функции входит куча неявных параметров кроме типов аргументов и результата. Получается проблема, аналогичная исключениям в C++. Впрочем, программисты C++ и CL, как правило, данный факт проблемой не считают.

В Scheme пропагандируется использование аргументов-функций для всех особых случаев (например, вместо restart-case в аналоге на Scheme будет аргумент-функция, которая будет вызвана при ошибке). В CL with-* — макросы, в Scheme call-with-* и даже dynamic-wind — функции. Поэтому максимально облегчена работа с такими аргументами. Использование же скрытых параметров наоборот резко критикуется, так как сильно усложняет отладку и написание надёжных программ.

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

может это локальная функция? что в ней лексического то?

Область видимости.

[1]> (defun f (x y) (list x y))
F
[2]> (flet ((list (x y) (+ x y))) (f 1 2))
(1 2)

Для сравнения, динамическая переменная:

[3]> (defvar *x*)
*X*
[4]> (setf *x* 1)
1
[5]> (defun y () *x*)
Y
[6]> (y)
1
[7]> (let ((*x* 2)) (y))
2

Функции в CL динамическими быть не могут.

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

Функции в CL динамическими быть не могут.

ну так, я об этом и говорил

Какой профит от разделения неймспейсов в CL (комментарий)

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

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

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

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

на самом деле связывание происходит все равно в рантайме

не совсем

Просто при лексическом связывании функция таскает с собой окружение

Вот это окружение формируется при компиляции. Адрес переменной уже не меняется.

а при динамическом она самостоятельна

При динамическом, окружение передаётся в функцию снаружи, а формируется это окружение ближайшим let, привязавшим динамическую переменную.

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

В теории да. Я каждый раз как почитаю про Схемку, так сразу «как там всё круто», а как начинаешь писать... хз, может быдлокодерские гены заставляют чувствовать себя в CL комфортнее. Что в теории Scheme продуманнее я не спорю. Хотя бы потому, что разработчики Scheme могли себе позволить всё обдумать заново, а не тупо сделать так, как было в коммерческих лиспах того времени.

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

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

Да ладно. Вы почитайте историю появления scheme. После появления Модели Акторов Карла Хюитта, Сассман и Стилл долго не могли в нее врубится, и они написали этот диалект, специально для того, чтобы понять теоретические основы Модели Акторов, и попытаться его выразить в терминах лямбда-исчисления. Судя по воспоминаниям Хьюитта, они ничего так и не поняли, и не выразили, Модель Акторов не выразима в терминах лямбда исчисления. Потом они этот язык немного видоизменили, и бросили в массы. То бишь, scheme не был результатом переосмысления предшествующих лиспов, это был другой, экспериментальный язык.

portquest2016
() автор топика
Ответ на: комментарий от Gentooshnik

А CL, кстати, был запилен именно под влиянием Scheme. И не тот ни другой не является, по сути, наследником тех старых лиспов.

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

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

Я и имел в виду, что Scheme - не результат переосмысления предыдущих лиспов.

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

Да ладно. Вы почитайте историю появления scheme.

Scheme 1970-х к R5RS имеет такое же отношение, как LISP 1960-х к CL.

они написали этот диалект, специально для того, чтобы понять теоретические основы Модели Акторов

Ну да. А Java была написана для программирования микроконтроллеров.

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

В 70-х писали как «ещё один игрушечный лисп». Только в 1998 (r5rs) появились макросы (гигиенические, да), а в 2006 (r6rs) появился syntax-case и макросы стали лучше Common Lisp'овых.

Появление r5rs и r6rs как раз и было результатом переосмысления недостатков (как их понимали преподаватели Computer Science) Common Lisp.

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

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

А на какой схемке пытался писать? На R5RS просто много чего не хватает.

Хотя в CL есть пара нюансов, которые резку упрощают процесс разработки, но затрудняют получение качественного результата: разработка в образе и monkey-patching (возможность изменить любой объект любого пакета в любой момент). Плюс слишком хороший отладчик.

Я как-то пытался проанализировать цели CL vs Racket: Анализ пользователей Common Lisp и Racket

В общем, на CL комфортнее писать, на Racket проще получить стабильно работающую систему, особенно если разработчиков больше двух.

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

У меня все системы стабильны, но разработчиков ни разу не было больше одного. Возможно в этом и дело.

Нет, пользователей больше одного. :D Несколько десятков, из которых программистов только я.

Хотя в CL есть пара нюансов, которые резку упрощают процесс разработки, но затрудняют получение качественного результата: разработка в образе и monkey-patching (возможность изменить любой объект любого пакета в любой момент). Плюс слишком хороший отладчик.

Вспомнил. Именно из-за отсутствия разработки в образе. Отладчик в Dr. Scheme (да, это был R5RS) не слишком, но достаточно хороший. monkey-patching тоже использовал, но лучше, конечно, править сами сорцы библиотеки, во избежание проблем со следующей её версией.

Из-за этого в программе проверяется только happy path

И в хороших библиотеках и в моих системках всегда есть тесты. Свои я прогоняю довольно часто.

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

Тред порадовал. tailgunner'ом и последней страницей. :D

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

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

Намеренно. С момента как Lisp стали двигать в разработку AI.

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

И в этом смысле, конечно, Racket уже не лисп. Идеологически, он является алголом с макросами и скобками.

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

(Пофиг уже, всё равно Talks)

На одной конференции по ФП докладчик рассказывал как они делали сложную математическую модель на Haskell, а остальную систему - на Smalltalk. Закономерность, похоже. Разработка в образе = быстрое прототипирование сложных систем.

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

Разработка в образе = быстрое прототипирование сложных систем.

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

Кстати, ты пишешь: «лучше, конечно, править сами сорцы библиотеки, во избежание проблем со следующей её версией». Это в смысле отправлять патч разработчику? А если разработчику оно нафиг не надо? Вот. например, https://github.com/Kalimehtar/cffi-objects/blob/master/redefines.lisp .

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

Вообще я имел ввиду сразу форкать. Если разработчику не нужно - то нет проблем. Если нужно - то примет патч. Если нужно, но патч не принимает, то управляемо мержить. С monkey-patch'ем ты

  1. Не отправишь патч разработчику вообще.
  2. Если ему это тоже случайно оказалось нужно, но проблему он решил по-другому, то новая версия может пройти незамеченной и принести проблемы.
Gentooshnik ★★★★★
()
Последнее исправление: Gentooshnik (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.