Владислав Жаринов писал(а):
PSV100 писал(а):
...
Есть потенциал делать менее развесистыми и управляющие алгоритмы. ...
Алгоритм (рис. 1а) представлен в двух текстовых вариантах (рис. 1б и 1в), добавим третий:
... Транслятор а-ля LiveScript в таких случаях будет генерировать код необязательно через реальные исключения, на выхлопе могут быть те же классические "if"-ы, goto, jump и т.д., что нужно на целевой платформе.
...
- Ваши предложения не связаны с обсуждавшимся здесь:
viewtopic.php?p=58038#p58038 (включая исходную ветку)? т.е. Вы хотите AND-THEN и/или PROGRESS-IF?
В целом, в исходном сообщении речь шла, в т.ч., и о технике реализации нечто подобного а-ля "PROGRESS-IF". Т.е. ставиться задача организовать алгоритм некоего управляющего характера, когда важно, или крайне желательно, содержательные и контекстные действия оформлять рядом, наглядно. Попробую раскрыть суть чуть подробнее.
Поскольку изначально примеры были на псевдокоде в стиле языка CoffeeScript/LiveScript, то и продолжу в том же духе. Подобные языки создаются для формирования "когнитивного" исходного кода, в частности, с императивной строгой (по умолчанию) семантикой, которые позволяют транслировать код как в целевой байт-/бинарный код, так и в исходники "низкого" уровня - JavaScript/C/C++/Pascal и т.д. Задаваемая формальная модель вычислений, где обработка ошибок м.б. реализована в т.ч. и через механизм исключений, позволяет на выходе в этом случае иметь как необходимые jump-переходы, так и, например, "нижний" код с задействованием флагов с анализом через if/switch, или goto. И ключевой момент - как организовать удобную обработку ошибок или анализ результатов ф-ций, легко, понятно, с минимум уровней вложенности кода и т.п.
В последнее время всё больше и больше становится модным принцип обработки ошибок в стиле Erlang-а, который себя оправдал на многолетней практике. Основная суть: в случае непредвиденной или нежелательной ситуации необходимо генерировать ошибку (исключение), прерывать работу процесса, раскручивать стэк, выполнять уничтожение объектов и освобождение ресурсов. При этом, как правило, в системе выстраивается иерархия процессов, где наверху м.б. процессы-супервизоры, которые контролируют своих подчиненных. Они получают сигналы о завершении работы своих наблюдаемых или связанных труженников, в т.ч. и об аварийных ситуациях. В результате надзиратели могут принять решение, чего же делать при крахе контролируемого. В основном, типовой сценарий - пусть процесс упадёт, зафиксируем этот факт в логе, запустим заново, разберёмся потом, а сейчас работать надо, без остановки. И такова архитектура позволяет в т.ч. контролировать и удаленные, распределенные, процессы. Основная мотивация для оправдания уничтожения процессов - в случае ошибок (исключений) крайне проблематично, в общем случае, особенно при наличии раскрутки стэка, восстановить или сформировать корректное состояние процесса, да и к тому же необходимо проектировать и реализовывать дальнейшую работу процесса после ошибок. Что в итоге только усложняет разработку и ещё больше способствует появлению глюков. Однако, это не значит, что на каждый чих нужно выбрасывать исключение. Немалая часть возникающих проблем вполне ожидаема и естественна, и их необходимо учитывать. К тому же желательно, чтобы библиотечный обобщённый код поменьше вмешивался в логику работы прикладного кода приложения. Поэтому часть проектируемой информации о проблемах содержится в результатах функций, где на всю катушку используются алгебраические типы (на счёт обработок ошибок в Erlang-е:
документация,
статья,
здесь пару слов).
И для обработки результатов функций применяется "сопоставление с образцом" (
pattern matching). Причём в Erlang-е нет "присваивания", знак "равно" означает именно сопоставление (где может быть и связывание переменных со значением). Это отличный приём для вооружения. Если сопоставление неуспешно, то автоматически генерируется исключение вида "bad match error...", и в объекте исключения (для дальнейшего анализа) можно зафиксировать место возникновения ошибки (отражающее условие сопоставления) и значение, с которым сопоставляли (т.е. то, что было во время вылета ошибки справа от "равно"):
Код:
x = 1
y = 2
~x = y # здесь возникнет "bad match..."
В коде выше добавленная к "х" закорлючка "~" означает не связывание с переменной, а сопоставление с уже хранящимся значением в "х" (кстати, в самом Erlang-е нет явных подобных указаний, и если раньше где-то переменная уже введена в строй, то автоматически выполняется сопоставление со значением, что иногда может приводить к неожиданностям, когда кто-то не заметил/забыл, что переменная с таким именем уже использовалась где-то выше по коду). Подобная техника позволяет расслабиться и не писать лишних типовых if/case/switch/raise/throw и т.д. для обработки результатов и выброса исключений, по таким мотивам (с заглавной буквы - конструкторы данных или "enum" и т.п.):
Код:
load_file = (name) ->
OK f = file::open(name, [Read, Binary])
OK bin = file::read(f, 1024*1024)
binary_to_data(bin)
Функции типа "file::open" обычно возвращают кортеж вида "OK(data) | Error(message)", и требуя "ОK" в нужных местах мы не пройдём вперёд, вылетим по исключению, и уже где-то наверху решат, чего делать.
Однако не всегда допустимы вылеты из процесса и его перезапуск. В каких-то случаях рестарт приведёт к излишнему переоткрытию файла, переконнекту по сети и т.п. В самом Erlang-е имеются механизмы для перехвата исключений по типовой схеме а-ля "try-catch/except-finally" (и некоторые др.). В исходном сообщении форума был затронут язык Go. Гугловцы передрали вычислительную модель Erlang-а (его модель акторов), в т.ч. и принципы обработки ошибок. Но для перехвата исключений схему "try-except-finally" (в отличие от С++ в Go нет деструкторов, поэтому им нужен и "finally") гугловоды критикуют. Подобные инструкции усложняют код, вносят новые уровни вложенности, размазывают логику на части (часть алгоритмов улетает в секции "except"), в случаях, когда необходимо по месту обработать ошибку и повторить попытки, возникают вложенные "try...try" и т.д. и т.п. В общем, рациональное зерно есть. Они придумали
Defer, Panic, and Recover (плюс
здесь). Вызов "defer" определяет "отложенные" вычисления, которые будут исполнены после завершения функции (причём именно функции, в отличие от деструкторов, которые должны отрабатывать сразу при выходе из области видимости и возможно и раньше, если объект больше не используется). В случае нескольких "defer" вычисления определяются согласно LIFO. "Recover" останавливает "панику", использоваться может только внутри "defer", прекращает раскрутку стэка, в результате есть возможность определить значение ф-ции в случаях неуспеха. Гугловцы рекомендуют возвращать ошибки как результаты ф-ций и "паниковать" только по делу. Но в Go нет сопоставления с образцом, анализ ошибок требует возни с if/switch, в общем, не всё так приятно, как хотелось бы. В результате сами же go-разработчики в базовых библиотеках интенсивно "паникуют", правда на "нижнем уровне" и стараются не пропускать панику за пределы интерфейса модуля, чего всем и советуют. И такая ситуация вызывает критику со стороны тех, кто привык работать в системах, где якобы нет неоднозначности как обрабатывать ошибки - через возврат кодов/объектов или через исключения - если приняты исключения, то хочешь или нет, но ты тоже вынужден будешь их интенсивно использовать, работая с кучей общераспространенных библиотек.
В общем, в Go есть нюансы с "паникой", а вот сами "defer"-вычисления, как таковы, очень даже ничего себе... Удобно реализовывать "finally":
Код:
load_file = (name) ->
OK f = file::open(name, [Read, Binary])
defer file::close(f) # указываем, что в конце ф-ции закрыть файл,
... # вне зависимости от исключений
OK bin = file::read(f, 1024*1024)
binary_to_data(bin)
И на счёт самих исключений... Основной недостаток типовой модели "try-except/catch" и "паник" в том, что при выбросе исключения в блоке "except/catch" потенциально существует возможность проанализировать ошибку и принять меры, но, в общем случае, вернуться в исходное место и повторить попытку не получится, имея в виду без дополнительных внешних телодвижений поверх try-except. Плюс пока включится "except/catch" произойдёт раскрутка стэка с соответствующим разрушением контекстного состояния. В связи со сказанным, модель исключений в лисп-классике гораздо эффективнее и гибче:
Conditions and Restarts (
перевод). Такая модель позволяет организовать обработку исключений без раскрутки стека (при необходимости), т.е. при возникновении ошибки на месте принять меры и повторить вычисления с учётом корректив, при этом принятие решения как обработать ошибку можно вынести на верхний уровень, т.е. в функции выше уровнем, которые вызывают данную процедуру, где возникла проблема. Пример:
Код:
# определим типы данных, через которые выразим возможные исключительные ситуации
# и ответные реакции на них:
BadFile(msg: str) = Retry(fname: str) | UseDefault # при ошибке открытия файла: открыть другой указанный файл или использовать файл "по умолчанию"
InvalidRead = SetValue(val: str) | SetZero # при ошибке чтения данных: использовать указанную строку или ноль
# определим ф-цию чтения данных, которая принимает имя файла и возвращает прочитанные данные,
# указав также то, что ф-ция может сигнализировать 2 сигнала об исключениях:
load_file: (str) -> SomeData(a) with BadFile|InvalidRead
load_file = (name) ->
# пытаемся открыть файл, в случае неуспеха посылаем сигнал BadFile с сообщением
# о проблеме и, если в ответ указали реакцию, делаем ещё одну попытку открытия необходимого файла:
try
OK(f) else BadFile(msg) = file::open(name, [Read, Binary])
handle
Retry(newname) => OK(f) = file::open(newname, [Read, Binary])
UseDefault => OK(f) = file::open(service::get_def_name, [Read, Binary])
# "отложено" закрываем файл
defer file::close(f)
...
# пытаемся прочитать данные, при неуспехе сигнализируем "InvalidRead" и применяем то,
# чего указали в ответ:
try
OK(bin) else InvalidRead = file::read(f, 1024*1024)
handle
SetValue(v) => bin = v
SetZero => bin = 0
# используем прочитанные данные...
binary_to_data(bin)
# применяем ф-цию чтения данных:
some_func = () ->
...
# в секции "with" указываем возможные реакции на сигналы, посылаемые из "load_file":
try
bindata = load_file(file_name)
with
BadFile _ => Retry(alt_name)
InvalidRead => SetValue("*invalid*")
...
# продолжаем работу...
use_data(bindata)
Секции "try-with-handle" похожи на типовые "try-except/catch", но есть семантические отличия. В случае исключения внутри "try..." мы сразу не вылетаем наверх, первоначально не выполняется раскрутка стэка, если где-то определён подходящий блок "with" (как и try-except и им подобные в типовых языках, возможны несколько блоков по уровням), то посылается соответствующий сигнал (условно, т.е. выполняются вычисления в блоке with), и исключение "трансформируется" в указанный ответ. Затем выполняется то, что определено в блоке "handle", вне зависимости от того были или нет задействованы "with". Если в "handle" не обработано исключение, то вылетаем по ошибке как обычно (операторы "try-with-handle" напоминают операторы для "pattern match" вида "case-of", т.е. в них возможны любые действия).
Таким образом, осуществляется проектирование ошибок и предусматриваются реакции на них, при этом существует возможность на месте, без раскрутки стэка и потери контекста, применять действия, плюс имеем возможность делегировать решение о стратегии восстановления выше стоящему начальству.
Запись вида " OK(f) else BadFile(msg) = file::open(name, [Read, Binary]) " есть сокращенная форма сопоставления:
Код:
case file::open(name, [Read, Binary]) of
Ok(f) => _ # т.е. ничего не делаем, просто связываем переменную "f" со значением
Error(msg) => fail BadFile(msg) # "выбрасываем" исключение, т.е. сигнализируем об ошибке
В блоке "with" мы можем выбросить новое исключение, соответственно уничтожив тех, кто старался выжить, или же прекратить их работу, например, через ф-цию "abort", где в качестве параметров необходимо задать результат вычислений:
Код:
some_func = () ->
...
bindata = try load_file(file_name) with
BadFile _ => if has_alt_name then Retry(alt_name)
else fail "Bad file ..." # решили упасть...
InvalidRead => case some_flag of
AsZero => SetZero
AsStr => SetValue("*invalid*")
NeedAbort => abort(0xFFFF) # прерываем работу ф-ции load_file и указываем
... # результат для bindata как 0xFFFF
Таким образом:
- желательно, чтобы стандартный (или базовой, широко применяемый) библиотечный код поменьше мешал логике работы конечного прикладного кода в системах. В этом случае лучше передавать ошибки как результат ф-ции. Для таких ф-ций как "file::open" проблемы с открытием файла вполне ожидаемы, и лучше в прикладном коде конкретного приложения решать падать или нет;
- применение механизма исключений, как выше, предназначено для возможности повторить проблемные вычисления без потери контекста и ресурсов, когда это важно. При этом предусмотренную политику действий может указывать управляющий код более высокого уровня;
- в иных случаях лучше "прибить" процесс, чтобы он дальше не работал глючным (да и сложно спроектировать всеохватывающею стратегию работы, с увеличением сложности больше рисков с ошибками), надзиратели переинициализируют вычисления, если необходимо;
- весьма полезны "отложенные" defer-вычисления, особенно когда желательно наглядно отразить действия компактно, рядом, в контексте. Они не заменяют, а дополняют деструкторы и технику RAII (и согласно примерчикам выше такая операция как закрытие файла должна быть оформлена как типовой деструктор для универсального применения, но при необходимости его автоматический вызов можно переопределить, типа "defer free file").
Ну и "сопоставление с образцом" - неплохой помощник. Вот такой вот вариант реализации PROGRESS-IF...
И ещё раз обращаю внимание, что для подобного кода блок-схемы - слабые помощники, особенно когда вместо явных циклов - рекурсии, передаваемые по месту "лямбды", применение "аспектного" программирования, как, например:
Код:
# где-то в каком-то модуле устанавливаем "перехваты":
#[turn before load_file]
check_before = (file_name) ->
log("before: " + file_name)
#[turn after load_file]
check_after = (file_name) ->
log("after: " + file_name)
#[turn when BadFile] # ф-ция сработает, когда возникнет сигнал исключения BadFile(msg: str),
check_bad_file = (err_msg) -> # параметр msg будет передан ф-ции
log("bad open: " + err_msg)
# в другом модуле используется load_file:
some_func = () ->
...
try
bindata = load_file(file_name)
with
BadFile _ => Retry(alt_name)
InvalidRead => SetValue("*invalid*")
...
do ->
some_func()
# в результате при исполнении some_func появятся сообщения в логе:
# "before ..."
# "bad open ..." если во время выполнения load_file был сигнал BadFile
# "after ..."
Так можно комбинировать вычисления, формировать разные варианты сборки приложения для тестирования, мониторинга и т.д., при этом не трогается базовый код.
В общем, для такого кода блок-схемы вынуждены запускать рабочую точку в скрытые туннели, или необходимо организовывать дополнительные боковые обвязки маршрутов через спец-иконы а-ля "таймер" + секции (например, как представлено
здесь, макроикона "группа действий с заданной длительностью"), и то, если получится, или уже задействовать 3D.
(модератор) выделено из viewtopic.php?p=83835#p83835 по указанию администрации раздела