LINUX.ORG.RU

Vim или Emacs? А LISP в 2021?

 , ,


1

3

https://www.youtube.com/watch?v=8Q9YjXgK38I&t=42s

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

А ведь Crashbandicoot была годной игрой…

Что выбрать? Vim или Emacs?
Изучать в 2021 году Lisp? Если изучать, какой? Практика?
А не засмеют сотрудики?

Времени в сутках маловато, на всё не хватает.


Ответ на: комментарий от byko3y

Какая разница, испортится ли внутреннее состояние в одном процессе сервера БД или всех процессах?

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

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

И ты мне скажешь, что Racket умеет в вызовы функций между задачами с околонулевыми накладными расходами?

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

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

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

То есть решение существует. А в линуксе как?

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

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

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

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

Отключение сетевого диска может давать более весёлые последствия, чем просто «повторить операцию?». Например, если используются блокировки, то благодаря «прозрачности» NFS в случаях перезапуска сервера клиенты могут наблюдать плохо предсказуемые явления, вроде прилета SIGUSR1 из ниоткуда с отпусканием блокировки, из-за чего два пользователя могут начать писать в файл и данные в нём превратятся в винегрет. Конечно, какая-нибудь Java не допустит порчи данных, потому по умолчанию приложение «безопасно» упадет.

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

А ты мне пишешь «Повторить, Игнорировать?» — гладко было на бумаге, да забыли про овраги. Повторить-игнорировать хорошо работает только на примитивных однозадачных системах без сети.

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

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

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

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

При чём тут новые классы в рантайме? Можешь добавить на string.Formatter.parse действие перед или после того, которое прописано в описании класса? В CLOS можно

Могу. Естественно, форматтеры и/или наследник string нужно будет наследовать от нового класса. Если ты про monkey patching, то это опасная практика, которая некогда была популярна в «скриптовухе», но даже там ее стараются избегать.

Так почему они не описываются как локальные переменные? Ответ — потому что в лиспе были или до сих пор есть убогие механизмы передачи контекста

У кого не убогие? Как должно быть правильно?

Явная ручная передача контекста, автоприменение контекста аля computational expressions из F# или монад хаскеля, ссылки на контекст в структурах данных и классах.

Что делать, если нужно все-таки прочитать контекст исходного потока — мне страшно подумать.

Передавать ссылку на него в параметре

Или сохранять исходный контекст в нединамические хранилища. Что, на самом деле, нужно было делать с самого начала.

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

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

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

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

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

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

Ты пишешь то, что в мудацких терминах называется «Inversion of Control». То есть, общие функции вызывают конкретную реализацию: serialize -> вызывает buffer_write -> вызывает system_write.

Теперь смотри, как это делается в случае так сказать «Normal Control».

def serialize(obj, buffer):
  ...
  buffer_write(obj_bytes, buffer)
  ...

def buffer_write(bytes, file):
  ...
  system_write(block, file)
  ...

init_serializer()
buffer = Buffer()
with open(get_store_place(), "w") as store_file:
    while serialize(obj, buffer):
        buffer_write(buffer, store_file)

close_serializer()

Знаешь, в чем прикол такого подхода? Я могу встроить этот цикл в главный цикл приложения и делать параллельно с этой операцией другие операции. Я могу получить сигнал отмены операции, я могу отреагировать на ошибку самыми разными способами, а не только «игнорировать-повторить».

Неужели лисп настолько застрял где-то в 80-х/начале 90-х, что тот порочный подход, который ты предложил, не режет тебе глаз? В той же Clojure, как я уже писал выше, есть подвиги в сторону поддержки асинхронщины.

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

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

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

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

То есть решение существует. А в линуксе как?

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

В линуксе давно есть футексы, которые позволяют синхронизироваться через разделяемую память, есть /dev/shm и системные вызовы для работы администрирования разделяемой памяти (конкретных имен уже не вспомню, сорян). io_uring — это, опять-таки, linux-only.

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

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

в конкретных языках, типа плюсов никто нормальный не юзает std::thread напрямую, а пишет свой класс BasicThread (навроде явского), от него наследуется для реализации конкретных тредов, приписывает там свои поля и прочие штуки, и все эти данные изолированы не хуже чем для процессов, если не расписывать память по случайным адресам разумеется.

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

вы просто не умеете их готовить и уповаете на «волшебные языки».

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

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

Пацаны это поняли намного раньше. Как по мне - корм из обезьян после Индианы Джонса - никакой. Не лезет что-то.

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

то исть в нашей деревне, треды юзают так -

class MyThread1 : public BasicThread {
//для пущей строгости поля приватные.
  int _my_field1=0;
  int _me_field2=0;

  //это тело вашего треда - перегруженный абстракный метод BasicThread
  void thread_body() override {

  }
}

MyThread1 mythread; //можно даже статически аллокировать, при этом тред на запустится, даже не будет создан в системе. реальное создание и пуск будут в момент когда скажут ему run.


...
mythread.run(); //пускает тред - тут тред оси создается и пускается в реальности.
alysnix ()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от byko3y

Корректный способ решить проблему — это при восстановлении подключения сравнить файл с исходным редактируемым файлом

Даже в Windows 10 при копировании файла теперь есть «Продолжить» в случае потери соединения. И ReGet придумали пару десятилетий назад.

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

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

Явная ручная передача контекста, автоприменение контекста аля computational expressions из F# или монад хаскеля, ссылки на контекст в структурах данных и классах.

Это всё в CL есть. Но динамические переменные удобнее.

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

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

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

Естественно. Что в этом плохого? Если в глубина рекурсии не растёт постоянно, то это нормальная программа.

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

Как этому мешает рекурсия? События обычно во втором потоке обрабатываются.

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

Теперь смотри, как это делается в случае так сказать «Normal Control».

У всех функций по-прежнему один лишний аргумент. Но теперь ещё и вызывающий код должен быть посвящён во все детали реализации. Если я в CL добавлю в сериализатор атомарность записи и у пользователя ничего не поменяется, то у тебя придётся переписывать код во всех местах, где был with open … while serialize

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

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

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

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

У всех функций по-прежнему один лишний аргумент.

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

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

Так ли это плохо, при наличии аргументов по умолчанию в языке?

У лисповой команды format есть 11 параметров. Если бы приходилось указывать все 11 для любой функции, которая использует форматированный вывод, это было бы ужасно. Даже с аргументами по-умолчанию. К тому же умолчать их можно только один раз во внешнем API. В дальнейшем все придётся передавать явно.

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

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

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

К тому же умолчать их можно только один раз во внешнем API. В дальнейшем все придётся передавать явно.

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

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

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

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

def g(a, b=1, c=2, d=3):
  ...
def f(a, b=1, c=2, d=3):
  ...
  g(a, b, c, d) # здесь надо указать все, так как какие-то могли быть переданы в параметрах
  ...

ещё и дефолтные значения в каждой функции дублировать. А если надо поменять дефолтное, то найти все места использования. Ад!

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

здесь надо указать все, так как какие-то могли быть переданы в параметрах

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

ещё и дефолтные значения в каждой функции дублировать.

Да, зато они могут быть разными на разных уровнях. То есть, у функции нижнего уровня выбрано как-то «универсально разумное» значение по умолчанию, а уровнем выше можно выбрать другое значение (и всё равно дать пользователю его настраивать). С динамическими переменными так разве получится?

А если надо поменять дефолтное, то найти все места использования. Ад!

Разве ситуация не один в один с (динамическими) глобальными переменными, если хочется поменять начальное значение? Может даже с параметрами проще - тут мы всё-таки ограничены одной функцией. А с глобальными переменными, которые использует множество функций, не разломать что-то становится ещё сложнее.

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

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

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

Это даёт максимальную гибкость как разработчику библиотеки (ему не надо в изначальный API обязательно указывать все возможные параметры и все возможные исключительные ситуации, их можно дописать позже без изменения пользовательского кода), так и пользователю библиотеки (партизанские заплатки, т.е. monkey patching, позволяют подкрутить функции библиотеки под свои нужды, не делая её отдельную ветку, что позволяет обновлять библиотеку при выходе новых версий).

Но всё-таки: в идеальном случае этот ад переместится в документацию, что совсем не факт, что будет лучше.

Посмотри количество переменных окружения в man bash. Даже у простенького ls есть 10 параметров. В документации они отлично живут. А теперь представь себе, что тебе пришлось бы их все перечислять в каждом вызове. Причём, если ls запускается из скрипта, то все 10 надо взять из аргументов скрипта и явно повторить.

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

С динамическими переменными так разве получится?

Часто так делают. В переменной пишется по умолчанию null, а в документации к функции указано, что null трактуется как 10, например.

Тогда в функции уровнем выше, если она хочет умолчание 20, будет

(let ((*param* (or *param* 20)))
  здесь, если *param* было не определено, оно равно 20
  ...)

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

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

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

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

Уже придумал, как двум таким «хорошо изолированным» потокам писать в один файл? Как дергать стороннюю либу так, чтобы по ночам не снилишь ужасы про «я дернул потокоопасную функцию». Мало того, чтобы свой hello world сделать безупречно безопасным — нужно также переписывать остальную инфраструктуру. И там по итогу все равно получится «дернул не то, не там, и всё завалилось».

Чтобы это решить в корне, нужно больше упора на неизменяемые и персистентные структуры данных, больше локальности — а это довольно сильно бьет по производительности, поскольку Тьюринг машины 50 лет назад были заточены именно под небольшое количество изменяемых ячеек данных: Фортран, Лисп, Кобол, Паскаль, Си — это всё яркие представители ЯП для Тьюринг машины с экономией и переиспользованием ячеек данных. Из-за их популярности сами аппаратные вычислители проектируются тоже в этом духе.

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

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

Даже в Windows 10 при копировании файла теперь есть «Продолжить» в случае потери соединения. И ReGet придумали пару десятилетий назад

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

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

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

No comments. Сегодня ты выступаешь за monkey patching, завтра будешь предлагать писать сборочные скрипты на ассемблере (ну а чо оно так долго собирает?) — тут сложно что-то противопоставить.

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

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

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

А я напоминаю, что синхронное программирование устарело уже в 90-х. Да. под него есть кучу устаревших готовых решений и инструментов отладки, но даже GUI уже давным-давно по большей части асинхронный, в десятых годах к асинхронности добавилась еще и независимость отрисовщика, из-за чего приложение даже не может простыми способами получить изображение того интерфейса, который оно сформировало, и всякие там paintEvent отправляются на свалку истории.

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

Естественно. Что в этом плохого? Если в глубина рекурсии не растёт постоянно, то это нормальная программа

Плохо то, что даже на уровне 10+ вложений программа даже в синхронном выполнении рискует сожрать сама свой хвост.

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

Как этому мешает рекурсия? События обычно во втором потоке обрабатываются

Общаются они как? Ты просто перекладываешь неудобный вопрос в другое место: проблему приложения не реагирующего на внешний ввод, превращаешь в проблему потока не реагирующего на другой поток.

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

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

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

Если я в CL добавлю в сериализатор атомарность записи и у пользователя ничего не поменяется, то у тебя придётся переписывать код во всех местах, где был with open … while serialize

Что еще за «атомарность записи»? fsync, что ли? В случае разделяемого хранилища так просто ты не отмахаешься — тебе придется еще и блокировочки реализовывать. Где ты это будешь делать? В сериализаторе? Сериализатор будет знать, как блокировать файлы, вносить атом изменений, и делать fsync/unlock? Очень интересный у тебя сериализатор.

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

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

Это не столько monkey patching, сколько AOP. Для Java есть AspectJ, например, но реализован через жуткие хаки-костыли на рефлексии.

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

У лисповой команды format есть 11 параметров. Если бы приходилось указывать все 11 для любой функции, которая использует форматированный вывод, это было бы ужасно. Даже с аргументами по-умолчанию. К тому же умолчать их можно только один раз во внешнем API. В дальнейшем все придётся передавать явно

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

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

Разве ситуация не один в один с (динамическими) глобальными переменными, если хочется поменять начальное значение? Может даже с параметрами проще - тут мы всё-таки ограничены одной функцией. А с глобальными переменными, которые использует множество функций, не разломать что-то становится ещё сложнее

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

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

Посмотри количество переменных окружения в man bash. Даже у простенького ls есть 10 параметров. В документации они отлично живут. А теперь представь себе, что тебе пришлось бы их все перечислять в каждом вызове. Причём, если ls запускается из скрипта, то все 10 надо взять из аргументов скрипта и явно повторить

ls и прочие никсовые утилиты — это каноничный пример негибкости. Ты можешь добавить новые переменные окружения, новые флаги, но ты уже никогда ничего не сможешь сделать со старым, неудобным интерфейсом ls. В ту же ловушку попал Git, который вместо API принимает только текстовые команды и выдает только текстовый выхлоп, весьма проблематичный в парсинге, и по итогу половину кода расширений/плагинов отводится только под парсинг выхлопа Git. Да, libgit2 обещает решение всех проблем, но это еще дело ближайшего будущего.

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

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

А это уже вопрос подхода.

Разные подходы - это ок, так можно выбрать что-то себе по душе. Но мы тут ради спора о «правильноm инструменте». (:

Это даёт максимальную гибкость как разработчику библиотеки (ему не надо в изначальный API обязательно указывать все возможные параметры и все возможные исключительные ситуации, их можно дописать позже без изменения пользовательского кода)

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

А вот с параметрами не понял: разве есть разница как расширять апи? Ни введение новых «глобальных» переменных ни добавление параметров (с дефолтными значениями) не заставят пользователя изменять свой уже написанный код.

Даже у простенького ls есть 10 параметров. В документации они отлично живут.

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

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

Да зачем при каждом?

Причём, если ls запускается из скрипта, то все 10 надо взять из аргументов скрипта и явно повторить.

Только если скрипту надо изменять все эти десять параметров. То есть, далеко не в каждом случае.

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

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

Часто так делают. В переменной пишется по умолчанию null, а в документации к функции указано, что null трактуется как 10, например.

Интересно, не догадался.

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

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

Это не столько monkey patching, сколько AOP. Для Java есть AspectJ, например, но реализован через жуткие хаки-костыли на рефлексии

Aspect Oriented Programming — это просто солидно и дорого звучащий синоним для Monkey Patching. Причем, так сложно сложно, как в AspectJ, приходится делать реализацию Monkey Patching исключительно из-за зашкаливающей убогости Java. В паскале/сихе не принято использовать обратные вызовы, и, как правило, подобное логирование делается минимальной копипастой и допиливанием клея, или же, как в динамических языках, можно сделать вообще произвольную структуру данных с похожим на стандартный интерфейсом и передавать ее нужным функциям. Но в случае Java ты не можешь просто создать потомка базового класса с измененным поведением, потому что от базового класса уже унаследовано куча потомков в стандартно либе, и теперь тебе придется всех их переписывать.

То есть, по сути, аспектно-ориентированное программирование явилось реакцией на отсутствие гибкости у Java. Всё, больше оно никому не нужно и нигде не упоминается. Но поскольку жава — это все-таки довольно популярный продукт, то вроде как получается «Великое и Всемирно Известное AOP» — хотя на самом деле это мелкий и малоприметный набор костылей для единственного из десятков только лишь популярных ЯП (и сотен менее популярных).

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

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

Я бы посоветовал здесь обозначить смысл слова «гибче». «Можно сделать больше разных изменений одним и тем же инструментом» — да, это вполне подойдет под определение «гибче», но так же оно подойдет под определение «переусложненно», «перегружено». В чем проблема использовать разные инструменты под разные задачи? В комбинации эти инструменты будут такие же «гибкие», как и один перегруженный инструмент.

Грубо говоря, это разница между велосипедом с одним колесом и велосипедом с двумя колесами — одноколесные велосипед проще и гибче, но перегруженнее и опаснее.

Другой, более канонический пример — макросы в Си. Они «гибкие», они «перегруженные», они «опасные» — они совмещают функции описания констант, обобщенных функций, и условной компиляции, и даже еще какие-то более редкие применения.

По итогу индустрия пришла к тому, что основная трудность программирования заключается в склонности человека ошибаться, потому «гибкость» в конкретно этом смысле слова в программировании категорически нежелательна. Я подчеркиваю, что это лишь определение гибкости одного аспекта инструмента — есть совершенно иное понятие «гибкости» у всей системы как поддатливости комбинации ее инструментов решению задачи. Например, как упомянуто выше, лисп позволяет писать DSL из коробки, но питон с минимальными телодвижениями позволяет писать тот же DSL через дополнительные инструменты. То есть: решение простых задач просто, а решение сложных задач возможно.

Короче говоря, проблема в том, что от вашего определения «гибкости инструмента языка» до «нечитаемая лапша, которую нельзя понять и модифицировать» всего один шаг — что кажется парадоксальным, как же гибкость приводит к негибкости? Но парадокс решается, если понять, что это разные «гибкости». Гибкость лиспа — это write-only программы, для модификации которых мы просто переписываем код заново. Что и происходило до того, как лисперов отправили ковырять вилкой питон.

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

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

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

«Выход из блока» происходит сразу же при запуске асинхронного алгоритма

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

то есть — запоминать и копировать это окружение при вызове определенных функций

Так в этом и есть суть динамической переменной. Только копирование происходит при попытке её изменить. Copy-on-write.

Плохо то, что даже на уровне 10+ вложений программа даже в синхронном выполнении рискует сожрать сама свой хвост.

Я списки на миллион элементов обрабатывал через (let loop (…) (cons … (loop …))). То есть хвостовой рекурсии нет, вложенность миллион. Проблем нет.

проблему приложения не реагирующего на внешний ввод, превращаешь в проблему потока не реагирующего на другой поток

Если нужна реакция на события, то они просто периодически опрашиваются. Опять же, как этому мешает рекурсия? В рекурсивной функции можно также проверять события. И с потоком проще: можно через interrupt-thread сделать в не реагирующем потоке любое действие.

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

Что еще за «атомарность записи»?

Файл с сериализованным объектом или есть целиком или его нет вообще. Реализуется через временный файл и переименование.

Сериализатор будет знать, как блокировать файлы, вносить атом изменений, и делать fsync/unlock? Очень интересный у тебя сериализатор.

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

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

макросы в Си. Они «гибкие»

В каком месте они гибкие? Они очень тупые. Но главная проблема с ними, что они как бы вне языка.

no-such-file ★★★★★ ()
Ответ на: комментарий от byko3y

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

Метод передачи параметров на последствия изменения поведения не влияет.

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

Проблема наследия есть в любом языке. От Кобола до JS (оператор равенства чинить нельзя).

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

Но мы тут ради спора о «правильноm инструменте»

Угу. Что правильней, молоток или отвёртка?

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

Все параметры с дефолтными значениями надо вытаскивать на уровень выше

f(a, b=1, c=2, d=3): ...
g(a, b=1, c=2, d=3):
  ...
  f(a,b,c,d)
  ...

выходит новая версия f(a, b=1, c=2, d=3, e=4). Теперь необходимо переписать g, иначе новый параметр пользователь g не может указать.

Да зачем при каждом?

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

Только если скрипту надо изменять все эти десять параметров. То есть, далеко не в каждом случае.

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

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

Синглтоны, объекты с коллекцией параметров, … Или просто не подозревают о такой возможности. Писали же люди на бейсике безо всяких классов, структур и даже функций.

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

Так и есть. Например, https://quickref.common-lisp.net/bordeaux-threads.html#Exported-special-variables

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

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

Тогда нельзя узнать, изменялось ли оно. Всё верно. Можно добавить в параметр

;; (f a *param*), если надо без изменений

(defun f (a &optional (param 42))
  (let ((*param* param))
    (g ...)))
monk ★★★★★ ()
Ответ на: комментарий от monk

Но обработка отличается от нынешней моды «выплюнуть стек и упасть»

Нельзя запретить писать плохие приложения. На любом языке.

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

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

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

Я списки на миллион элементов обрабатывал через (let loop (…) (cons … (loop …))). То есть хвостовой рекурсии нет, вложенность миллион. Проблем нет

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

проблему приложения не реагирующего на внешний ввод, превращаешь в проблему потока не реагирующего на другой поток

В рекурсивной функции можно также проверять события

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

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

И с потоком проще: можно через interrupt-thread сделать в не реагирующем потоке любое действие

interrupt-thread бесполезно за пределами отладочных целей. Ну типа грохнуть поток можно в любых языках — а вот какой ЯП позволит грохнуть поток так, чтобы данные из него остались в корректном состоянии? Я таких не знаю. Потому потоки стараются не останавливать.

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

Файл с сериализованным объектом или есть целиком или его нет вообще. Реализуется через временный файл и переименование

Ну так реализуй временный файл и переименование — зачем это совать в сериализатор?

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

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

byko3y ★★★ ()
Ответ на: комментарий от no-such-file

В каком месте они гибкие? Они очень тупые. Но главная проблема с ними, что они как бы вне языка

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

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

Метод передачи параметров на последствия изменения поведения не влияет

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

Проблема наследия есть в любом языке. От Кобола до JS (оператор равенства чинить нельзя)

У меня в коде JS эдак на 100 тыс строк и плюс еще лям сторонних библиотек нет ни одного оператора равенства. Де факто починено.

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

Aspect Oriented Programming — это просто солидно и дорого звучащий синоним для Monkey Patching.

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

Причем, так сложно сложно, как в AspectJ, приходится делать реализацию Monkey Patching исключительно из-за зашкаливающей убогости Java.

И я об этом.

В паскале/сихе не принято использовать обратные вызовы

Э-м, не понял, при чём тут обратные вызовы?

В Паскале, точнее в Delphi, ещё точнее в VCL, у многих методов есть дополнительные методы BeforeBlaBla и AfterBlaBla. В CLOS это просто сделано универсально для всех методов вообще.

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

По моим ощущениям, в Java-мире он и не пользуется особой популярностью.

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

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

Кто ж спорит-то? Но одно дело, когда это некоторые заранее разработанные и оттестированные слоты, а другое дело — когда мы переписываем поведение случайных классов.

В паскале/сихе не принято использовать обратные вызовы

Э-м, не понял, при чём тут обратные вызовы?

Обратные вызовы, виртуальные методы, интерфейсы — это всё разные сорта одного говна.

В Паскале, точнее в Delphi, ещё точнее в VCL, у многих методов есть дополнительные методы BeforeBlaBla и AfterBlaBla. В CLOS это просто сделано универсально для всех методов вообще

Не, Object Pascal — это отдельное убожество, я писал про обычный паскаль, потому и поставил его рядом с Си.

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

Обратные вызовы, виртуальные методы, интерфейсы — это всё разные сорта одного говна.

Э-м… Все в машинуна Хаскел?

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

Нельзя запретить писать плохие приложения. На любом языке.

Язык определяет параметры программы по-умолчанию. Если программа на Java, она будет разбита на классы, если программа на Haskell, в ней будет отделён ввод-вывод от основного алгоритма, если программа на C++, она будет использовать RAII, если программа на Common Lisp, в ней будет отладчик. Понятно, что настоящий программист может написать программу на Фортране на любом языке, но в среднем язык достаточно сильно влияет на архитектуру программы.

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

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

Попробуй написать что-то, что будет менять данные из вызывающей функции

В смысле, изменять переданную структуру? Тоже работает.

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

Можно подробнее? Или пример? Чувствую непонимание (то ли я тебя, то ли ты меня).

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

interrupt-thread не грохает поток, а выполняет функцию в контексте потока. Там можно и состояние проверить и поменять что надо.

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

Ну так реализуй временный файл и переименование — зачем это совать в сериализатор?

А кто это должен делать? Пользователь сериализатора каждый раз велосипедить?

Это называется «модульность».

Модульность – это когда сериализатор внутри разбит на модули, а не когда вместо решения дают полуфабрикат.

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

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

Статические анализаторы легко проверяют. Даже тупо grep ‘\*\S*\*’ по телу функции достаточно.

Де факто починено.

Но ведь не делают нормальный, а то вдруг где-то используется.

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

Для работы на удаленном сервер vim #1. Нет ничего удобнее. Для локального легкого блокнота есть sublime, vscode и прочие, они быстрые, красивые, достаточно мощные. Для полноценной разработки большого проекта различные IDE основанные на IDEA, PyCharm например. Для EMACS имхо вообще нет кейсов использования больше.

Aswed ★★★★★ ()
Закрыто добавление комментариев для недавно зарегистрированных пользователей (со score < 50)