ФлеймФорумПрограммирование

Левоионные идеи в сфере программирования (51 стр)

Страницы: 147 48 49 50 51 52 Следующая »
#750
5:15, 26 июня 2025

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

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

#751
13:57, 26 июля 2025

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

Итак, допустим, у нас есть некая библиотечная функция, принимающая на вход объект с интерфейсом CharGetter:

interface CharGetter() {
 int getChar(); // или -1, если конец потока
}

function parseStream(CharGetter stream): List<Token> { ... }

И вот мы пытаемся её задействовать для задачи "распарсить поток из файла":

FileStream fileStream = openFile(...);
var tokens = parseStream(new CharGetter() {
 int getChar() { return fileStream.isEof() ? -1 : fileStream.readChar(); }
});

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

Можно подправить адаптер, чтобы исключения ловились сразу, и такие ситуации приравнивались к eof, но это неправильно - это всё же ошибки, и желательно направить поток контроля более адекватным способом.

Можно ловить их традиционно:

try {
var tokens = parseStream(new CharGetter() { ... });
} catch (IOException e) {
... // предполагаем, что это у нас вылетело именно из fileStream, потому что вроде как больше неоткуда
}

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

parseStream(...) {
 var someHelperThatCanThrowIOException = ...;
 try {
 ...
 } catch (IOException e) {
 ... // предполагается, что это может быть только от someHelperThatCanThrowIOException
 }
}

А решить это затруднение можно, например, добавлением ссылки на объект-источник ошибки (причём при этом даже тип ошибки оказывается не так сильно нужен):

FileStream fileStream = openFile(...);
try {
var tokens = parseStream(new CharGetter() { ... });
} catch (e {fileStream}) {
... // тут поймается только то, что выкинулось из методов fileStream
}

И мы уже не сломаем логику parseStream, которая, в этом случае, может быть такой:

parseStream(...) {
 var someHelperThatCanThrowIOException = ...;
 try {
 ...
 } catch (e {someHelperThatCanThrowIOException}) {
 ... // это точно может быть только от someHelperThatCanThrowIOException
 }
}

Стоит также заметить, что популярное в современных веяниях решение типа "возвращаем алгебраический тип Result | Error" - говно, недалеко ушедшее от кидка типизированного исключения. В нашем примере это соответствует "ловить ошибку сразу же в адаптере CharGetter". Ну вот мы поймали её там, и что? Адаптер может вернуть вызвавшей его логике или символ, или eof, а ни один из этих вариантов не отражает произошедшее адекватным образом. Что дальше - поднимать панику? Но в нашей ситуации есть вполне определённое место в коде, в котором можно отреагировать именно на эту ошибку. С помощью костылей и проктологии, конечно, можно обойти проблему, но это уже явный признак ущербности механизма.

#752
17:44, 26 июля 2025

Sbtrn. Devil
> А решить это затруднение можно, например, добавлением ссылки на объект-источник ошибки (причём при этом даже тип ошибки оказывается не так сильно нужен)

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

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

#753
(Правка: 18:38) 18:35, 26 июля 2025

Sbtrn. Devil
У тебя изначально ошибка в том, что ты пытаешься привязаться ко внутренним кишкам функций, которые ты вызываешь. Когда решаешь практические задачи, рано или поздно приходишь к тому, что эксепшон де-факто нужен всего лишь один — "подпрограмма не смогла завершить работу", а что и почему — для бизнес-логики это уже не интересно, это пусть пользователь/программист/куэйщик по логам и трейсбекам уже разбираются вручную. Максимум что может заинтересовать саму программу во время исполнения — это какие инварианты остались ненарушенными, условно на примере какого-нибудь гипотетического фотошопа — или это только операция в плагине не смогла нарисоваться, или весь файл теперь в неопределённом состоянии и надо его перезагрузить, но остальные файлы в том же окне в порядке и их трогать не надо, или же это вообще глобально и везде и осталось только перезагрузить компьютер. Но это определяется общей архитектурой приложения — как в принципе устроены все эти сессии/операции/плагины; и модификации к механизму эксепшонов к этому — примерно как материал обоев к танко-устойчивости небоскрёба.

#754
19:32, 26 июля 2025

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

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

И неизменно, при встрече с реальной жизнью, все эти начинания настигает один и тот же конец — всевозможные сценарии поломок на практике оказываются намного, намного, намного более многочисленными, чем авторы предполагали в теории — и рано или поздно, но обязательно доходит до сценария «оно просто сломалось, я ниипу уже почему, мой енум с кодами ошибок итак уже простирается на 15 экранов, последние 36 часов я вообще ничего не делал кроме как добавлял константы в енум и дописывал к ним везде ветки во всех свитчах, я устал, я не хочу уже больше этого делать, просто прими как данность что всё сломалось и ты ничего уже с этим не поделаешь», также известного как "InternalError", "ApplicationError", "UnknownError", а иногда даже "InvalidData". И как только появляется такой универсальный поглотитель — то и все остальные фейловые сценарии, как под гравитационной тягой чёрной дыры, стягиваются в эту "универсальную ошибку". И все остальные механизмы начинают только мешать.

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

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

А вот эти вот все чекеды, резулты и так далее — это всё трубочки для выпивания моря. Они не помогут тебе обработать прямое попадание метеорита прямо в RAM точно в тот момент когда ты делаешь «total += arr».

#755
3:01, 27 июля 2025

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

> У тебя изначально ошибка в том, что ты пытаешься привязаться ко внутренним кишкам функций, которые ты вызываешь.
С точностью до наоборот. У нас есть чётко определённое место, где ошибка чтения файла с целью компиляции может произойти - getChar адаптера, и чётко определённое место, в котором на эту ошибку можно (и нужно) реагировать - обёртка вокруг parseStream. Эта конкретная ошибка, при возникновени в первом месте, должна быть доставлена во второе. И в этом деле мы не хотим зависеть от внутренних кишков parseStream. Это, допустим, третьепартное гов изделие, которое про ситуацию "тот адаптер, который дал мне вызывальщик, обосрался при чтении из файла" не знает, и не должно. Поэтому возможностей вмешаться в доставку ошибки, которая его не касается, у него не должно быть в принципе. Это и есть суть задача механизма.

> также известного как "InternalError", "ApplicationError", "UnknownError", а иногда даже "InvalidData".
Заметь, что в приведённом примере не важно, как именно обосрался getChar. Важно, что обосрался именно он (или, по крайней мере, присутствует в цепочке обосравшихся). Именно этот факт обеспечивает отличие ситуации "файл штатно закончился" от "файл не прочитался". Но в традиционном подходе с результатами или чекед-ексепшенами мы не можем выразить этот факт (без костылей): нужно или задавить ошибку, чтобы втиснуть в интерфейс parseStream, в котором возможно только "файл штатно закончился", или афтор parseStream должен заранее заложиться, что в CharGetter::getChar может случиться неведомая херня, и добавить это в интерфейс (в духе getChar() throws ParserGetterWrappedGenericError и parseStream(CharGetter) throws ParserGetterWrappedGenericError), со всеми вытекающими. А при подходе "оно просто поломалось" мы вообще ничего не можем.
Зато, если ошибка привязывается к её виновнику, всё сразу становится ко всеобщему удовлетворению. parseStream ничем не заморачивается, для него дефолтная реакция "всё плохо, раскручиваем стек и кидаем дальше" более чем достаточна и не требует лишних движений. Верхний уровень имеет гарантию, что поймает то, что ловит, тогда и только тогда, когда произойдёт именно та конкретная неприятность в том конкретном контексте, который он ожидает. И это достигнуто без введения новых видов ошибок.

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

#756
(Правка: 18:22) 18:16, 27 июля 2025

Sbtrn. Devil
> Это, допустим, третьепартное гов изделие, которое про ситуацию "тот адаптер, который дал мне вызывальщик, обосрался при чтении из файла" не знает, и не должно. Поэтому возможностей вмешаться в доставку ошибки, которая его не касается, у него не должно быть в принципе. Это и есть суть задача механизма.
Ну обычно, если третья либа принимает коллбеки, то там как правило обозначается и конкретный механизм, по которому коллбек может обозначить неисправимую ошибку и потребовать ранний выход — считай, это часть сигнатуры коллбека. Вот этим и надо пользоваться. А кидать эксепшоны сквозь чужой код — это вообще УБ, ибо там может вообще другой ABI оказаться, так что стекораскрутитель от твоего throw вместо нормальных указателей увидит мусор и сделает говно.

Sbtrn. Devil
> а) у нас нет гарантии, что мы поймаем здесь IOException именно от нашего файла, а не от хрен знает чего в глубине parseStream, что у них считается за нештатную ситуацию, и потому не обрабатывается.
А тебе и не важно. Парсинг не достиг успешного результата — вот и всё что тебе надо знать.

Sbtrn. Devil
> б) у нас нет гарантии, что parseStream внутри не устроен как-нибудь так:
См. выше про обозначение ошибки из коллбека.

Sbtrn. Devil
> Заметь, что в приведённом примере не важно, как именно обосрался getChar. Важно, что обосрался именно он (или, по крайней мере, присутствует в цепочке обосравшихся). Именно этот факт обеспечивает отличие ситуации "файл штатно закончился" от "файл не прочитался".
Конец файла должен обозначаться явным образом, потому что это штатная ситуация, например как ты сам написал — через возврат специального еоф-маркера вместо очередного символа.

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

#757
18:23, 27 июля 2025

Sbtrn. Devil
> афтор parseStream должен заранее заложиться, что в CharGetter::getChar может случиться неведомая херня
Да, должен. Если не заложился — смело выбрасывай либу на помойку и ищи другую.

#758
0:05, 28 июля 2025

Имбирная Ведьмочка
> А кидать эксепшоны сквозь чужой код — это вообще УБ, ибо там может вообще другой ABI оказаться, так что стекораскрутитель от твоего throw вместо нормальных указателей увидит мусор и сделает говно.
Ну, ситуации типа "сверху донизу трагический ппц" мы и не рассматриваем - там о механизмах обработки ошибки речь надо вести с вопроса "оно вообще хоть как-то возможно?". В таких орхетектурах, как правило, и с калбаксами тоже очень трудно.

> А тебе и не важно. Парсинг не достиг успешного результата — вот и всё что тебе надо знать.
Вот мы и имеем ситуацию, когда в парсинге не предусмотрено возможности не достичь успешного результата. И, если
> прямо посреди файла пользователь физически выдернул флешку из компьютера.
то прореагировать на это как-то иначе, чем уронить программу с "generic fatal error" из калбакса, средств нет.

Ну, точнее, если очень нужно, то можно извратиться, но штатно их нет.

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

Афтор-то не поленится. Заложится и на ошибку в калбаксе. И ещё в 100500 коллабораторов, которые от него тоже не зависят, но знание, что зафейлились именно они, может быть нужно верхнему уровню. Всё это у себя внутри тщательно обмажет и вывесит тебе наружу в виде throws SubParserFailure1, SubParserFailure2, ... SubParserFailure100500 (а ты думал, все эти ApplicationError и InvalidData берутся только из желания наплодить побольше сущностей?). И язык позаботится, чтобы верхний уровень никакой из этих вариантов не пропустил. Полегчает ли тебе от этого?

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

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

#759
(Правка: 0:59) 0:27, 28 июля 2025

Sbtrn. Devil
> Именно по такой логике и изобрели чекед иксепшоны.
Нет, то что изобрели по такой логике — это HRESULT и аналоги. У настоящей либы такого рода, настоящий интерфейс был бы:

interface ICharGetter() {
    HRESULT GetChar(OUT int* pChar);
}

Если считывается символ: *pChar = код символа; return S_OK. Если достигли конца файла: *pChar = -1; return S_OK. Если случилось непоправимое: return errcode такое что errcode<0.

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

Sbtrn. Devil
> Афтор-то не поленится. Заложится и на ошибку в калбаксе. И ещё в 100500 коллабораторов, которые от него тоже не зависят, но знание, что зафейлились именно они, может быть нужно верхнему уровню.
Опять же, нет, не надо ничего из этого делать. С точки зрения автора либы — просто обозначь механизм, по которому коллбек может вернуть ошибку, и в случае если этим механизмом воспользуются — просто вызови деструкторы и передай ошибку наверх как есть. "Подпрограмма не смогла выполнить поставленную задачу", и не надо ничего усложнять там где это не нужно. Одна ошибка на все случаи жизни — это просто, а что просто — то надёжно.

А пользователь, если ему надо, уже сам заморочится в адаптере — вплоть до thread_local std::exception_ptr, в колбаке сохранил и вернул E_FAIL, на изначальном вызове увидел E_FAIL и ре-кинул обратно дальше.

#760
13:35, 28 июля 2025

Имбирная Ведьмочка
> А пользователь, если ему надо, уже сам заморочится в адаптере — вплоть до thread_local std::exception_ptr
У нормальных коллбаков есть, как минимум, один пользовательский параметр, в который можно загнать указатель на произвольный объект. В нем можно хоть лог ошибок вести, костыли с глобальными переменными или thread_local не нужны.

#761
(Правка: 15:03) 15:00, 28 июля 2025

}:+()___ [Smile]
> У нормальных коллбаков есть, как минимум, один пользовательский параметр, в который можно загнать указатель на произвольный объект. В нем можно хоть лог ошибок вести, костыли с глобальными переменными или thread_local не нужны.
Байтики жалко, это же на каждый вызов надо будет по отдельной экскпшон-коробке заготавливать, хотя в большинстве случаев в одном треде у нас больше одного эксепшона за раз и не бывает.

#762
16:58, 28 июля 2025

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

> А пользователь, если ему надо, уже сам заморочится в адаптере — вплоть до thread_local std::exception_ptr, в колбаке сохранил и вернул E_FAIL
Вот это как раз я и назвал "если очень нужно, то можно извратиться". А оно как раз очень нужно. И это у нас только один источник ошибки и один пользователь. А если цепочка сложнее? Скажем:

operationDoer(session) {
  var parser = session->getParser();
  var file = openFile(...);
  Error inCallback = null; // допустим, будем ловить сюда
  var parseResult = parser->parse(new CharGetter() {
    char getChar() { ... inCallback = error; }
  });

  if (inCallback) {
    logError("Failed to read file");
    return EVERYTHING_IS_BAD; // ошибка уровня операции, но мы не умеем их дифференцировать
  }
  ...
}

Parser::parse(charGetter) {
  ...
  char = charGetter->getChar() || return EVERYTHING_IS_BAD;
  ...
  // но теперь он может вызывать изнутри себя также и методы разных сессии, которые тоже могут зафейлиться
  if (!this->session->performSessionLevelAction(...)) return EVERYTHING_IS_BAD;
  ...
}

masterDoer() {
  var session = sessionPool->getSession();
  for (;;) {
    var result = operationDoer(session);
    if (result != OK) {
      ... // если зафейлилась только операция - делаем continue
      ... // если случился фейл уровня сессии - делаем break
      // но мы не можем, потому что определить эту разницу нечем
    }
  }
}

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

#763
22:07, 28 июля 2025

Sbtrn. Devil
> А если у нас более сложный парсер, у которого ошибка может случиться не только в юзерском калбаксе, то что он будет наверх передавать?
Ничего:

ParseResult parse_file(char const* path) {
    struct FCharGetter {
        MyFile f;
        std::exception_ptr ex;

        HRESULT static get_char(FCharGetter* cg, int* pout_char) {
            try {
                if (f.at_eof())
                    *pout_char = -1;
                else
                    *pout_char = f.get_char();
                return S_OK;
            } except (...) {
                ex = std::current_exception();
                return F_FAIL;
            }
        }
    } cg;
    cg.f = MyFile(path);
    ParseResult pr;
    HRESULT hr = run_generic_parser(&cg, &cg::get_char, &pr);
    if (SUCCESS(hr)) {
        return pr;
    } else {
        if (cg.ex)
            std::rethrow_exception(cg.ex);
        else
            throw_run_generic_parser_intrinsic_failure(hr);
    }
}

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

#764
3:07, 29 июля 2025

Имбирная Ведьмочка
> if (cg.ex)
> std::rethrow_exception(cg.ex);
> else
> throw_run_generic_parser_intrinsic_failure(hr);
Так по твоей парадигме parse_file тоже должен возвращать HRESULT.

> Только разумеется, если это встречается чаще одного раза в твоей программе
Конкретно run_generic_parser может встретиться один раз, а сам такой паттерн - вполне себе 100500. Например, следом за распарсиванием кучки юнитов идёт qsort, в котором в качестве калбакса - компаратор с контролем консистентности, затем запуск на выполнение пачки вычислений и reduce их результатов с опять-таки кастомным калбаксом (в случае первого же найденного фейлового результата редукцию и возможное выполнение оставшихся можно и нужно прерывать, но стандартный reduce такому не обучен), итп.

Страницы: 147 48 49 50 51 52 Следующая »
ФлеймФорумПрограммирование