Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Новости и прочее

Обработка ошибок

С развитием человечества технологические риски увеличиваются.
Подушек безопасности нет — умрём как мужики
В программах не лишне иметь и подушки безопасности,
и предохранители, и «защиту от дурака».
Чем больше техника входит в нашу жизнь, тем выше цена, которую мы платим технические ошибки — как допущенные по недомыслию, так и преднамеренные. За примерами далеко ходить не надо. Шведская биржа была парализована лотом на покупку -6 фьючерсов: число -6 было истолковано как 4294967290. Стоимость получившегося лота в 131 раз превысила ВВП Швеции. В другом случае француженка получила счет на 12 квадриллионов евро. А вот небезобидный случай: авария ракеты «Ариан-5» из-за ошибки в программе системы инерциальной ориентации. Некорректное преобразование 64-битного числа с плавающей точкой в 16-битное целое со знаком принесло ущерб в сотни миллионов долларов.

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

О борьбе за правильность программ формальными методами

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

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

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

Ошибка — состояние не удивительное

            Значительная часть функций при определённых обстоятельствах не могут возвратить запланированный результат запланированного типа. Мы постоянно сталкиваемся с ситуациями, когда функция может не вернуть правильного значения. Например, мы где-то что-то не нашли, не прочитали, не получили. Пытались преобразовать строку в число, но строка содержала нечисловые символы. Хотели получить электронное письмо, но не вышло: то ли почтовый сервер не ответил, то ли писем просто не было. Этот особый случай — не повод завершать программу. Проблемы неизбежны: то окружающий мир несовершенен (не читается сайт), то «железо» для программы несовершенное (переполнения при арифметических операциях).

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

Значения-исключения как признак ошибки

            В разных языках проблемы обнаружения ошибок и их обработки решаются по разному. В языке Си применён старинный рецепт борьбы с ошибками: выбирается определённое значение, не входящее в область значений функции, оно принимается за признак ошибки. Это так называемое значение-исключения, которое возвращается при обнаружении ошибки. Для указателей такое значение равно нулю. Например:
// признак ошибки — нулевой указатель
int* ptr = get_ptr (parameter);
if (ptr)
	*ptr = value;
else
	cout << "ошибка: нулевой указатель\n";
            Недостатки этого значений-исключений таковы:
  • Зачастую область значений функции занимает весь диапазон значений возвращаемого типа, нет «лишнего» значения, из-за этого не обеспечивается универсальность проверки. То есть этот способ не годится для построения всеобъемлющей системы обработки ошибок.
  • Когда же значения-исключения возможны, они нередко имеют разные значения и подбираются для каждого конкретного случая.
  • Необязательность проверки: программист её может опустить и компилятор этого не заметит.
Кроме того, язык не предусматривает контроля переполнений и других ошибок при выполнении арифметических операций.

            Вот такая она — система проверки ошибок в Си: она неполноценна, по факту её нет. А то, что есть — это не система. Специальные синтаксические конструкции и ключевые слова в языке отсутствуют. Обходятся «подручными средствами». Такая картина с обработкой ошибок присуща, конечно не только Си, но многим другим языкам.

            Значения-исключения не могут стать основой для универсальной и всеобъемлющей системы обработки ошибок. Потому что в одном случае значением-исключением является ноль, в другом наоборот не ноль, а в третьем вообще какое-то «магическое» число. И часты случаи, когда вся область значений функции занята, и для данного индивидуального случая нет места значениям-исключениям. Такая «индивидуальная зависимость» исключает универсальность.

Обработка ошибок в C++, Rust, Haskell

            В C++ проверка ошибок основывается на конструкции try — catch — throw. Недостатки:
  • Тяжеловесность.
  • Необязательность проверки: программист её может опустить и компилятор этого не заметит.

            Примечание. Генерация исключения посредством «throw» в одной из реализаций C++ влечёт генерацию следующего кода:

   ;			throw;
	push      0
	push      0
	call      @_ReThrowException$quipuc

Тут не всё просто: вызывается «@_ReThrowException$quipuc», которой передаются два нулевых параметра. Т.е. ни установка каких-то флагов, ни установка какого-то значения переменной, ни переход на ветку «catch» — ни один из «дешёвых» вариантов не задействован. Вызывается функция, которая ничего не знает о той ветке «catch», где она будет обрабатываться. Значит, эту ветку функция будет просто искать. А вот окончание ветки «catch»:

	call      @@_CatchCleanup$qv

Зачем тут вызов ещё какой-то фунции, что за «очищение catch»? Простоты решений тут нет.

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

            В языке Rust другой подход к обработке ошибок. Проверки правильности полученных из функций значений легковесны, делаются на том же уровне, что и основные вычисления. Функции, которые могут вернуть ошибку, возвращают значение типа Option, Option<&T> или Result. Это обёртка над основным типом T.

            Option<&T> (обёртка для адресов) для своей реализации дополнительной памяти не требует, признаком ошибки является нулевой указатель.

            Option хранит в себе собственно объект типа T плюс признак ошибки. Размер Option — это сумма размера T, размера признака ошибки (не менее 1 байта) и байтов, необходимых для выравнивания:
размер (Option) = размер (T) + 
      размер (признак ошибки) + байты выравнивания
            Result хранит в себе признак ошибки и, в зависимости от наличия ошибки, либо объект типа T, либо объект типа "ошибка". Размер Result — это сумма большего из размеров T или E, признака ошибки (не менее 1 байта) и байтов, необходимых для выравнивания:
размер (Result) = max (размер (T), размер (E)) +
      размер (признак ошибки) + байты выравнивания
            Объекты типов Option и Result занимают дополнительную память. Небольшую, да занимают, это является недостатком этого метода обработки ошибок. Если тип T — это целое число, то функция, возвращающая объекты типов Option или Result, не сможет возвратить результат только в регистре. В отличие от функции, возвращающей целое число без признака ошибки:
   MOV  EAX, результат
   RET	; возврат
            В Haskell обработка ошибок возложена на монады Maybe и Either. Монада Maybe либо возвращает значение (Just), либо не возвращает (Noting). Монада Either аналогична Maybe, только в случае неудачи вместо Nothing позволяет уточнить причину ошибки. Монады Maybe и Either служат тем же целям, что и типы Option и Result в языке Rust. Что-то подсказывает мне, что и их реализация на уровне машинного кода аналогична.

Обработка ошибок малой кровью

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

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

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

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

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

            Примечание. Д.Ю. Караваев фразу «вычисление синуса или косинуса всегда корректно» прокомментировал так: «Увы, в нашем несовершенном мире это не так: при порче памяти вместо числа IEEE-754 на вход FPU подается мешанина нулей и единиц в виде «не числа» и синус/косинус также дадут исключение».

            Поскольку функция с потенциально отсутствующим значением сопровождается специальным ключевым словом (допустим, ключевым словом «ненадёжная»), то появляется возможность сделать проверки на ошибки обязательными. Т.е. правила языка в принудительном порядке обязывают производить эти проверки, иное компилятор не позволит.

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

    // Определение функции — начало
    . . . ненадёжная . . .
    // Тело функции
    // Код, предшествующий обнаружению ошибки


    // передаём характеристику ошибки от этой функции в вызвавшую
    ошибка = получить значение характеризующее ошибку ()
    )    // Определение функции — конец
            Как это выглядит в программе? Пусть f, g, h — ненадёжные функции, тогда:
Обработка ошибок
            Любая ошибка, случившаяся при вызове f, g или h, приводит к тому, что ветвь вычислений до ключевого слова «при ошибке» немедленно прекращается и начинается выполнен кода между ключевым словом «при ошибке» и закрывающей скобкой. Если ошибка произошла при выполнении h, то g и f уже не выполняются. Если ошибка произошла при выполнении g, то f не выполняется.

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

Примечание. Синтаксис конструкции обработки ошибок не понравится любителям LL(0)-грамматик. Но он краток, что соответствует философии языка.

            Эти блоки могут быть внутри друг друга:
(
    // ветвь 1
    (
         ветвь 1.1
     при ошибке
         ветвь 1.2
    )
     при ошибке
    // ветвь 2
    (
         ветвь 2.1
     при ошибке
         ветвь 2.2
    )
)
            В рассмотренном выше примере сразу три функции f, g и h могут стать источниками ошибок. Но какая именно и от какой поступил код ошибки — непонятно. Если важно знать источник ошибки, то проверку ошибок надо разнести в разные блоки:
tmp = 
( . . .
    h(x)
    при ошибке
    // реакция на ошибку в h
)
tmp =
( . . .
    g(tmp)
    при ошибке
    // реакция на ошибку в g
)
y =
( . . .
    f(x)
    при ошибке
    // реакция на ошибку в f
)
Мы видим, что блоки с проверками ошибок могут располагаться как последовательно, так и один внутри другого.

            Функция, которая при определённых условиях может выдать некорректный результат, сопровождается ключевым словом ненадёжная. Тогда в её теле при этих определённых условиях записывается ошибка = <код ошибки> или просто ошибка, если кода ошибки нет. «Ошибка» — это ещё одно ключевое слово.

            Ключевые слова ненадёжная и ошибка идут в паре, их раздельное употребление является синтаксической ошибкой. Функция, проверяющая ошибки, сама может быть ненадёжной и быть источником ошибок. Например, получив сигнал об ошибке от какой-то вызванной функции, она может передать его дальше в вызывающую функцию:
( ... ненадёжная ...     // определение ненадёжной функции
    f(x)	         // вызов ненадёжной функции f(x)
    при ошибке
    ошибка = код ошибки  // код ошибки, полученный из f(x)
)
            Ненадёжная функция не может быть употреблена иначе, как внутри конструкции обработки ошибок. Вызов ненадёжной функции за пределами такой конструкции вызывает синтаксическую ошибку. Таким образом обеспечивается гарантированная, а не факультативная проверка наличия ошибок. Это не серебряная пуля, но способствует повышению надёжности программ.

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

            Предложенная конструкция позволит проверять обычные арифметические операции на переполнения или деление на ноль без лишних нагромождений: y =
( ...
    m * n + p / q - r
    при ошибке
    // реакция на ошибки в цепочке вычислений
)
            Ненадёжность является одним из признаков, характеризующих функции. Нельзя вызвать по указателю ненадёжную функцию как надёжную. Хотя обратное возможно: проверка признака ошибки не даст положительного результата.
// так правильно:
указатель на ненадёжную функцию = указатель на надёжную функцию
// так не правильно:
указатель на надёжную функцию = указатель на ненадёжную функцию

Реализация с использованием флага

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

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

            В вызываемой функции в кодах x86 это может выглядеть так:
...
; ветвь алгоритма для выдачи правильного результата
   CLС                   ; очистка флага, нет сигнала об ошибке
   RET                   ; возврат из функции
...
; ветвь алгоритма, сигнализирующая об ошибке
   MOV	EAX, error_code  ; код ошибки
   STС                   ; установка флага, сигнал об ошибке
   RET                   ; возврат из функции
            В вызывающей функции в кодах x86 это может выглядеть так:
...
   CALL	func_334         ; вызов ненадёжной функции
   JC	ERROR_253        ; переход на обработку ошибки
   MOV	result_334, EAX  ; действия при отсутствии ошибки
            Установленный флаг, являясь сигналом об ошибке, нужно проверять сразу, пока он установлен. Зачем признак ошибки сохранять в какую-то переменную, а потом её анализировать, как это сделано в Rust? Ведь этот анализ всё равно в итоге приведёт к установке какого-то флага и условному переходу!

Реализация с модификацией адреса возврата и дополнительным переходом

            Д.Ю. Караваев, познакомившись с предварительной версией этой статьи, подсказал более короткий способ указания на возникшую ошибку. Он его воплотил в своём трансляторе языка ПРОЛ-2. Далее — текст Дмитрия Юрьевича.

            [В трансляторе ПРОЛ-2] применялась технология «послать на три байта». В те далекие времена подпрограммы начинались прологом
push bp
mov bp,sp
а заканчивались эпилогом
mov sp,bp
pop bp
ret 
            Так вот, было принято правило, что в случае положительного результата в программе ещё выполняется команда add w ptr [bp]+2,3 и подпрограмма передает управление не в точку после вызова, а на три байта ниже. Как раз для того, чтобы убрать сами проверки на нижний уровень:
call функция
jmp ошибка
... продолжаем безошибочное выполнение.
            Т.е. тоже самое, только без флага переноса. Этот прием оказался практичным и наглядным, например, разбор (на ассемблере) в трансляторе ПРОЛ-2 какого-нибудь выражения в скобках выглядел так:
call открывающая скобка
jmp это_не_выражение_в_скобках
call выражение
jmp ошибка_выражения_в_скобках
call закрывающая_скобка
jmp ошибка_нет_закрывающей скобки
... продолжение разбора 
            Это позволяло даже на ассемблере легко собирать сложные системы. Потом тот же прием был применен в программе автоматического обращения русско-немецкого словаря в немецко-русский и снова с успехом.

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

Дополнение к предыдущему подзаголовку

            Так получилось, что Дмитрий Юрьевич, описав мне проверку в предыдущем подзаголовке, получил возможность посмотреть исходники ПРОЛ-2. Это было непросто, потому что они были записаны на 5-дюймовых дискетах. Поди сейчас найди для них дисководы!

            И оказалось, что в ПРОЛ-2 это было реализовано по другому. Почти 30 лет прошло — неудивительно, что детали стёрлись. Но поскольку описанный в предыдущем подзаголовке метод тоже является работающим вариантом, то обирать это из статьи не стоит. Дальше — слово Д.Ю. Караваеву.

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

        POP BP  
        ADD BP,3
        PUSH BP
        RET
              При этом параметры через стек в трансляторе вообще не задавались и регистр BP не использовался в других целях. Теперь по исходным текстам это начало все вспоминаться. Вот пример одного из почти 200 модулей транслятора (разбор составного оператора): 
СоставнойОператор:
        MOV ВИД,0Ah
        CALL IM11
        PUSH ССЫЛКА_СОСТ
        MOV ССЫЛКА_СОСТ,0
M1:   CALL Блок
        JMP M6 ; Это Блок
M2:   CALL Выбор
        JMP M6 ; Это Выбор
M3:   CALL Переключатель
        JMP M6 ; Это Переключатель
M4:   CALL Цикл
        JMP M6 ; Это Цикл
M5:   CALL Ожидание
        JMP M6 ; Это Ожидание
MK:   CALL IM12
        POP ССЫЛКА_СОСТ
        POP BP  ; НЕ НАЙДЕНО НАЧАЛО ОПЕРАТОРА
        ADD BP,3
        PUSH BP
        RET
M6:   CMP ССЫЛКА_СОСТ,0
        JZ M7
        MOV ВИД,57H
        CALL IM11
        MOV AX,ССЫЛКА_СОСТ
        CALL IM17
M7:   POP ССЫЛКА_СОСТ
        MOV ВИД,59H
        CALL IM11
        RET
            Описание самого языка как документа не нашел, но теперь по исходникам транслятора можно восстановить даже мелочи. Вызов подпрограмм IM11, IM12, IM17 — это составление промежуточного вида программы (нечто вроде байт-кода). По сути разбор составного оператора — это последовательная попытка найти нужную конструкцию, если это не она — продолжаем разбор дальше, иначе выходим с отрицательным ответом (на три байта).

Реализация с двумя адресами возврата

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

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

            Как это воплотить? Надо сделать так, чтобы при выходе из подпрограммы в успешном случае управление передавалось на команду, следующей за CALL. При ошибке — на ветку обработки ошибки. Но как это сделать? Ведь самое наилучшее устройство подпрограммы — это когда она является «чёрным ящиком»: она ничего не знает о внешнем мире, всё, в чём она нуждается, передано её в качестве параметров. Значит, второй адрес возврата должен быть передан ей в неявном виде. На в языке высокого уровня это никак не отразится на синтаксисе, всё будет проходить «за кулисами». На уровне машинного кода это приводит к включению в генерируемый код команды
PUSH <адрес обработчика ошибок>
для функций с ключевым словом «ненадёжная». Т.е. при засылке параметров в стек в него поместится ещё один неявый параметр.

Заключение

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

Опубликовано: 2019.01.20, последняя правка: 2019.02.10    20:51

ОценитеОценки посетителей
   ██████████████████████████████████████████ 2 (100%)
   ▌ 0
   ▌ 0
   ▌ 0

Отзывы

     2019/01/20 18:48, utkin          # 

В C++ проверка ошибок основывается на конструкции try — catch — throw. Недостатки:
— Тяжеловесность. — Необязательность проверки: программист её может опустить и компилятор этого не заметит.

RTTI? Не, не слышал...

Монады Maybe и Either служат тем же целям, что и типы Option и Result в языке Rust.

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

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

Ну вот и сразу же ошибка, которую язык распознать не способен. Вообще-то практически вся тригонометрия выдает некорректные ОКРУГЛЕННЫЕ значения, там, в идеале как в ПИ до фига знаков после запятой. И, естественно, оно в стандартные типы не вмещается. Поэтому всё, что Вы подадите, всегда будет исковеркано в результате ошибок округления.

Поскольку функция с потенциально отсутствующим значением сопровождается специальным ключевым словом (допустим, ключевым словом «ненадёжная»), то появляется возможность сделать проверки на ошибки обязательными. Т.е. правила языка в принудительном порядке обязывают производить эти проверки, иное компилятор не позволит.

Первое — это медленно. Второе — разные функции могут давать разные ошибки и по нескольку сразу. Какие будем проверять? Третье на какие ошибки проверять функции, написанные программистом? Ведь если бы он сам об этом знал, то никогда бы эти самые ошибки не стал бы делать в своих программах, верно?

Вся дальнейшая логика по вложенной проверкt ошибок это обалденные — тормоза. Попадёте пару раз во вложенный цикл и — абзац Вашей программе.

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

     2019/01/21 11:05, Автор сайта          # 

RTTI? Не, не слышал...

Три совершенно разных пункта опровергается одним доводом. Логика? Не, не слышал.

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

А почитать то, что написано выше? Не, не надо. Не читать же сюда пришёл, а критиковать.

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

А почитать то, что написано выше? Не, опять не надо. Это статья — для вечно сомневающихся интеллигентов, которые пишут, но всё равно сомневаются, что будет работать. Поэтому они не могут гарантировать, что «fread» всегда успешно что-то прочитает. Функция «fread», будучи определённой как ненадёжная, будет проверена на успешность. А неуверенный в себе программист будет уверен, что файл будет либо прочитан, либо ему сообщат о неуспехе. Но Вашем языке «Валентина» будут программировать только сильные люди, у которых файлы всегда прочитаются читаются. Даже если диск уже десять лет, как сломался и лежит на свалке. К чему проверки сильным людям?

первое, что я вижу в Вашей системе — фатальные тормоза.

А почитать про замедление на доли процента в трансляторе PL/1? А зачем?

Д.Ю. Караваев: я вообще не могу заметить изменение скорости выполнения от использования данных команд. Вероятно, оно тоже составляет малые доли процента.

Не, мы же Нострадамусы.

     2019/01/21 11:36, kt          # 

Вспомнил такой штрих: в библиотеке математических функций БЭСМ-Алгол (примерно 1969-70 гг) в некоторых функциях был входной параметр — метка, на которую передаются управление (в обход выхода из процедуры) в случае ошибки. Т.е. та же идея — спрятать все проверки правильности внутрь подпрограммы и не заставлять проверять ошибку на верхних уровнях, уже тогда витала в воздухе. В Алголе-62 можно было передавать метку как параметр.

     2019/01/21 13:02, utkin          # 

Три совершенно разных пункта опровергается одним доводом. Логика? Не, не слышал.

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

А почитать то, что написано выше? Не, не надо. Не читать же сюда пришёл, а критиковать.

Так объясните. Я понял так. Поэтому написал.

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

Сейчас это работает. Нет проблем в существующих языках. В чем особенность-то?

А почитать про замедление на доли процента в трансляторе PL/1? А зачем?

Ещё раз — Вы предлагаете делать проверку, которой сейчас нет. Это не доли процента.

Т.е. та же идея — спрятать все проверки правильности внутрь подпрограммы и не заставлять проверять ошибку на верхних уровнях, уже тогда витала в воздухе. В Алголе-62 можно было передавать метку как параметр.

Чем это отличается от try-except? Только формой выражения в программе. Механизм работы тот же.

     2019/01/21 16:02, kt          # 

Ещё раз — Вы предлагаете делать проверку, которой сейчас нет. Это не доли процента.

Доли процента (м.б. даже тысячные доли) как раз между проверкой на переполнение с помощью INTO и полным её отсутствием.

Чем это отличается от try-except? Только формой выражения в программе. Механизм работы тот же.

Тем, что в Алголе и PL/1 это появилось на 30 лет раньше. И, используя этот опыт, надо было обработку ошибок сразу вводить в языки типа Си и Паскаль, а не присобачивать потом, выдавая за инновации.

     2019/01/21 18:20, utkin          # 

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

Тем, что в Алголе и PL/1 это появилось на 30 лет раньше.

Потому что тогда прерывания были панацеей. Сейчас как это работает?

     2019/01/21 18:36, utkin          # 

В С++ есть опция для функции noexcept и throw, которые указывают, что функция "хорошая" и не может генерировать исключения и "плохая". В чем разница? С++ 14.

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

Я не пойму идеи автора. Как его система исключений даст преимущества перед тем, что есть сейчас? Как работает какой-то ЯП?
а / 0
Проверка — деление на нуль? Генерация исключения (не знаю как, может и установкой переноса). Как предлагает автор?
а / 0
Проверка — деление на нуль? Установим перенос.

В чем тут магия? Я не говорю, что его идея плоха. Я просто не вижу преимуществ. Автор же пишет, что в С++ и прочих всё это громоздко и криво сделано. Значит его решение лучше по каким-то параметрам. Вот по кучи он там тесты давал и показал разницу — в 29 раз. Чётко и ясно. Тут же я не увидел, в чём фишка?

     2019/01/25 23:50, Автор сайта          # 

Ещё раз объясняю, почему важна проверка ошибок. Потому что значительная часть функций при определённых обстоятельствах не могут возвратить запланированный результат запланированного типа. Функция «прочитать текстовый файл» возвращает строку с текстом. Что она должна вернуть, если файл не читается? Пустую строку? Это так называемое «магическое значение», которое неуниверсально 1) из-за своего значения (другие функции могут вернуть другой «магический тест») и 2) из-за своего типа (другие функции могут вернуть, допустим, плавающее число, а не текст). Тангенс и котангенс не всегда могут вернуть правильное значение, а когда случается неправильное, не могут сообщить об этом. Арифметические операции подвержены переполнениям. Поэтому желателен универсальный механизм реагирования. И программист сам решает, как написать ему функцию. Если он объявляет функцию ненадёжной, то прибегает к универсальному механизму, а его код легко сопровождается его коллегами. А если использует значения-исключения, как признак ошибки, то теряется универсальность и обязательность проверки. Функцию можно будет вызвать, но результат не проверить.

Преимущества предложенной проверки таковы.
  • Она предлагает унифицированный и универсальный механизм. Ничего не придётся изобретать, всё единообразно.
  • Эта проверка обязательна, это исключает «забывчивость», повышает качество кода (почитайте блог компании-производителя PVS Studio, о «забытых» проверках нередко пишут). Много ли языков, где такая проверка обязательна?
  • Вообще не требует никакой дополнительной памяти, никаких дескрипторов, как в Rust. Проверка задействует регистры, а не память.
  • Генерирует минимально возможный машинный код.
В C++ эта конструкция тяжелее и в машинных кодах, и в затратах времени на выполнение этого кода.

Пример на C++
#include <stdio.h> 
int F (int arg) {
if (arg == 0)
throw;
return arg;
};
main() {
try {
printf("Good: %i \n", F (0));
}
catch (...) {
printf("Bad\n");
}
return 0;
}
генерирует такой код:
   ;	int  F (int  arg) {
push ebp
mov ebp,esp
push ebx
mov ebx,dword ptr
; if (arg == 0)
test ebx,ebx
jne short @2
; throw;
push 0
push 0
call @_ReThrowException$quipuc
add esp,8
; return arg;
@2: mov eax,ebx
; };
pop ebx
pop ebp
ret
; main() {
push ebp
mov ebp,esp
add esp,-36
mov eax,offset @@_$ECTC$@main$qv
push ebx
push esi
push edi
call @__InitExceptBlockLDTC
; try {
mov word ptr ,8
; printf("Good: %i \n", F (0));
push 0
call @@F$qi
pop ecx
push eax
push offset s@
call @_printf
; }
mov word ptr ,0
add esp,8
jmp short @7
; catch (...) {
; printf("Bad\n");
push offset s@+11
call @_printf
pop ecx
; }
mov word ptr ,16
call @@_CatchCleanup$qv
; return 0;
@7: xor eax,eax
mov edx,dword ptr
mov dword ptr fs:<0>,edx
; }
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
Обратите внимание на такой код:
   ;			throw;
push 0
push 0
call @_ReThrowException$quipuc
К чему приводит генерация исключения? К тому, что вызывается «@_ReThrowException$quipuc», которой передаются два нулевых параметра. Т.е. ни установка каких-то флагов, ни установка какого-то значения переменной, ни переход на ветку «catch» — ни один из «дешёвых» вариантов не задействован. Вызывается функция, которая ничего не знает о той ветке «catch», где она будет обрабатываться. Значит, эту ветку функция будет просто искать. А вот окончание ветки «catch»:
	call      @@_CatchCleanup$qv
Зачем тут вызов ещё какой-то фунции, что за «очищение catch»? «Нет, такой хоккей нам не нужен».

Вот поэтому программисты C++ не советуют использовать исключения, как вещь повседневного обихода, пишут о «боли, которая может возникнуть, например, в C++ при использовании try-catch». Замеров, как сильно тормозит try-catch в С++ не делал, поэтому «скока вешать в граммах» — сказать не могу.

Про C++ 14 сказать ничего не могу. В языке C++ начал разочаровываться до новых стандартов.

Да, в языках много маркетинга.

Маркетинг работает вовремя продаж или накануне. Тут ни о какой готовности к продажам речи не идёт. Как раз случается маркетинг наоборот. Придумал много лет назад одну фичу, потом в 2009 г. начал изучать PHP и увидел, что идея там уже реализована, но наполовину. В 2012 г. начал работать этот сайт. В 2015 г. появляется новый язык программирования, а его автор просит меня разместить информацию о нём на странице «отечественные разработки». В этом языке фича реализована не наполовину, как в PHP, а полностью, как описано на этом сайте. «Совпадение? Не думаю!».

     2019/01/26 08:30, utkin          # 

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

Например, не строку. Сейчас же, вообще не эта функция занимается решением данной проблемы. И это ОДИН ИЗ ВОЗМОЖНЫХ подходов к решению данных задач — проблемы должны решать специалисты. Это отлично сочетается с реалиями нашей жизни, то есть со специализацией по профессии. Врач не может всё починить в доме — и электрику, и сантехнику, и крышу, и одежду заштопать. Есть специалисты, которые решают этот вопрос лучше него и экономят его время. Как и за всё в жизни такое преобразование требует своей платы, в данном случае это дополнительные ресурсы, которые нужны программе для обработки ошибок.

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

Но такие функции не используются стандартно. А потом, функция может возвращать структуру из полей:
1) Значение
2) Результат операции
И проблем нет — нет исключения в программах, раз уж они Вас не устраивают и Вы можете отследить проблему САМИ, не автоматически.

Эта проверка обязательна, это исключает «забывчивость», повышает качество кода (почитайте блог компании-производителя PVS Studio, о «забытых» проверках нередко пишут). Много ли языков, где такая проверка обязательна?

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

Вот поэтому программисты C++ не советуют использовать исключения, как вещь повседневного обихода, пишут о «боли, которая может возникнуть, например, в C++ при использовании try-catch».

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

     2019/01/26 18:52, Автор сайта          # 

В языке ассемблера после выполнения команды сравнения (допустим, «CMP») совсем не обязательно делать условный переход (допустим, «JZ»). Можно между ними ещё несколько команд поставить и даже подпрограммы вызывать. Но в языках высокого уровня сразу после «if» идёт ветка «then». Обязательно идёт «then»! Хотя казалось бы: к чему такая обязаловка? Может, я ещё что-то хочу сделать между «if» и «then»? Нет, это не самодурство, это просто разумная дисциплина, уменьшающая вероятность ошибки.

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

проблемы должны решать специалисты

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

функция может возвращать структуру из полей:
1) Значение
2) Результат операции

Конечно может. Почти во всех языках программирования может.

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

Он легковеснее, т. к. реализуется минимальным числом команд процессора. Когда возникают ошибки, тогда возникает и необходимость реагирования на них. Программисты, которые заботятся о качестве своего кода, будут сами делать проверки и даже не заметят, что синтаксис к этому принуждает (ну как с «if» и «then»). А кто неаккуратен, тот конечно будет возмущаться: «Караул, меня проконтролировали, что я не смыл за собой в туалете! Меня ограничавают в творчестве!».

Получил письмо от Д.Ю. Караваева, после котого сделал дополнение к статье.

     2019/01/26 19:46, Comdiv          # 

Поднята хорошая тема.

Правда, новый механизм работы с исключениями в ЯВУ раскрыт не до конца. Расскажите пожалуйста:
1. Как функция должна сообщать об исключении?
2. О возможности или запрете транзитной передачи исключений от вызываемых функций к вызывающей.
3. О поддержке в языке развития кода, когда, например, функции, изначально помеченной как гарантированно выполняемой, потребовалось вызвать функцию, выполнение которой с исключением не позволяет выполнить контракт.
4. Сколько функций, по Вашему мнению, в среднестатистическом коде будут помечены как гарантированно выполняемые, и как это должно отразиться на ЯВУ?
5. Как обрабатывать функции, которые в общем случае не могут завершиться без исключений, но в предоставленных условиях должны?
6. Будут ли ошибки программы отделены от ошибок данных?

     2019/01/26 23:34, Автор сайта          # 

1. Как функция должна сообщать об исключении?

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

2. О возможности или запрете транзитной передачи исключений от вызываемых функций к вызывающей.

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

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

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

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

Значительная часть функций ненадёжна. Ввод и вывод — почти все подпрограммы не гарантируют безошибочности работы. Редкие исключения — это типа get_time() или set_time(). И то неуверен. Ведь был уверен за sin() и cos(), да мне Дмитрий Юрьевич глаза открыл.

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

Не понял сути вопроса. Они либо правильно работают, либо неправильно. И и том, и о другом мы должны знать.

6. Будут ли ошибки программы отделены от ошибок данных?

Желание такое: конструктор либо создаёт объект какого-то типа и далее объект существует, либо не создаёт, если возникла ошибка, тогда и объект не существует. Но данные могут приходить извне и содержать ошибки. Поэтому окончательное мнение ещё не выработано.

     2019/01/27 04:26, Comdiv          # 

1. Положить в регистр аккумулятор

Я имел ввиду вид в ЯВУ, а не в транслированном машинном коде. Как программист будет писать код, сообщающем об ошибке? В статье это не указано.

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

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

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

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

4. Значительная часть функций ненадёжна...

Как это учитывается в концепцию языка? На что будет похоже написание кода с представленной концепцией обработки исключений?

5. Не понял сути вопроса. Они либо правильно работают, либо неправильно. И и том, и о другом мы должны знать

Вот мы и знаем, что при заданных аргументах функция должна отработать без исключений, несмотря на то, что помечена как "ненадёжная". Язык обяжет и такой код заворачивать в обработчик ошибок? Пример:
  sum = add(a % 8, b % 16)
Как мы её обработаем? Также как и
  sum = add(any1, any2)
Или как-то по-другому?

6. окончательное мнение ещё не выработано.

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

     2019/01/27 05:48, MihalNik          # 

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

Не понял сути вопроса. Они либо правильно работают, либо неправильно.

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

     2019/01/28 07:41, utkin          # 

Но в языках высокого уровня сразу после «if» идёт ветка «then». Обязательно идёт «then»! Хотя казалось бы: к чему такая обязаловка? Может, я ещё что-то хочу сделать между «if» и «then»?

Дичайший аргумент. Может быть такая обязаловка потому что это ПРАВИЛО языка? Вам такой ответ в голову не приходил? Если в описании языка написано, что после проверки условия на истинность должно что-то следовать после then, значит именно такого поведения я и ожидаю в программе. Если Вы хотите сделать что-то ещё между if и then, то создаете свой новый оператор, который визуально отличается от if и поэтому может иметь поведение, отличное от данного же оператора.

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

Пусть это будет правило, всем будет наплевать, если об этом правиле будет написано ЯВНО. Что перед нами не if, а совершенно другая форма условия. Нет проблем, делайте на здоровье. Когда же Вы всем говорите, что в языке есть if, которые вовсе не if, то это не борьба с ошибками, а их порождение.

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

И да, и нет. Я за то, чтобы программист САМ решал, когда проверять ему ошибки. Если я вижу неоднозначность, данную волей хаоса (например, та же работа с файлами), которая от меня не зависит, я буду проверять такой код. И для этого в имеющихся языках есть годные средства. Если же лично я складываю а + б, я не буду его проверять на ошибки. Потому что если туда не поместится результат числа, то проблема не в РЕАЛИЗАЦИИ, а в ПРОЕКТЕ. По проекту помещаться туда число ДОЛЖНО. А если не помещается, то это снова косяк уже при наборе программы и отслеживать такую ошибку во время выполнения нет смысла.

Это очень известная проблема и она связана с тем, что программисты занимаются совсем не тем, что от них требуется. Программист должен ПИСАТЬ программу по ПРЕДСТАВЛЕННОМУ проекту, ТЗ и т.д., в котором программный инженер написал что и как должно быть. Когда же программист сам изобретает велосипед (что бывает пупец как часто), то таким образом он уходит от ответственности на этапе ПРОЕКТИРОВАНИЯ, не желает исправлять ошибки и переносит работу с ними в этап ПРОГРАММИРОВАНИЯ. Что в любом случае всегда приводит к потере времени и средств.

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

А сейчас именно так и происходит. Посмотрите в С++ реализацию деления. Операция деления САМА генерирует исключение и далеко не всегда это прерывание процессора.

Конечно может. Почти во всех языках программирования может.

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

А кто неаккуратен, тот конечно будет возмущаться: «Караул, меня проконтролировали, что я не смыл за собой в туалете! Меня ограничавают в творчестве!».

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

Очевидно причины, которые Вы указываете в качестве обоснования Вашей системы, не совсем корректны. Не понятно, почему Ваша система будет лучше имеющихся решений. Более того, использование новых значений в старых терминах (как изменение поведения оператора if) само является источником ошибок. Есть общеустоявшееся поведение оператора if в БОЛЬШИНСТВЕ языков программирования. Поэтому программисты будут уверены, что ВАШ if ведет себя аналогично. Поэтому Вы теперь в своих описаниях языка просто ОБЯЗАНЫ каждый раз упоминать, что ВАШ if – это не тот if, который if.

     2019/01/28 13:28, Автор сайта          # 

2 Comdiv

Надо отметить, что думая над обработкой ошибок, я вдохновлялся тремя источниками. В Rust мне понравилась легковесность обработки ошибок, в Haskell – обязательность, а C++ стимулировал поиски способов обойтись без дополнительных расходов. Получилось решение в духе минимализма, которое, впрочем, не исключает других способов обработки ошибок. Как с стиле Rust, так и в стиле C++.

Я имел ввиду вид в ЯВУ

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

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

Хороший вопрос. Честно говоря, дальше того, что описано выше, пока не думал, но вопрос про «многоэтажность» подталкивает к этому. Задумка была такая: если текущая функция вызывает ненадёжную и при этом не занимается обработкой ошибки, то сама имеет ключевое слово «ненадёжная», а в ветке «при ошибке» сигнализирует об ошибке
при ошибке                                                // ветвь обработки ошибок
ошибка = получить значение характеризующее ошибку () // передаём характеристику ошибки от вызванной функции в вызвавшую
То есть в этом особо осмысленных действий по обработке нет, идёт банальная передача информации от вызванной функции к вызвавшей. Такую малую информативность и полезность можно назвать информационным шумом.

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

Как тогда выглядит такая «транзитная» функция? Ключевое слово «ненадёжная» остаётся при ней, ветвь обработки ошибок («при ошибке») исчезает из кода на ЯВУ, но остаётся в неявном виде в сгенерированном ею машинном коде.

Ещё обдумаю, потом внесу правки в текст выше.

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

Надёжная функция может содержать в себе ненадёжные, если
  • функция обрабатывает ошибки вызванных функций, т. е. имеет ветвь обработки ошибок «при ошибке»,
  • сама не является источником ошибок и не передаёт информацию об ошибке по иерархии сверху вниз:
    ошибка = ...
Это разработчик принимает решение, должна ли функция обрабатывать ошибки сама или этим займётся вызывавшая функция. Т.е. атрибут ненадёжности не является обязательным, от него можно избавиться. Это в Haskell при вызове монадической функции типа Maybe или Either вызвавшая функция сама становится типа Maybe или Either. Здесь такого нет.

Язык обяжет и такой код заворачивать в обработчик ошибок? Пример:
sum = add(a % 8, b % 16)

Ещё раз скажу: Вы задаёте хорошие вопросы. Они стимулируют мозговую деятельность:). В Вашем примере видно, что аргументы имеют уменьшенную разрядность, поэтому переполнение невозможно. Поэтому проверка наличия ошибок бессмысленна, ибо их быть не может. Можно вспомнить старый язык PL/1, в котором имелись целые числа произвольной разрядности, например, BIN FIXED(24) – 24-разрядное целое. Или обратиться к LLVM IR, в котором есть типы типа i1, i2, ..., i31, i32, ... Если опереться на терминологию LLVM IR, то операцию sum можно определить так:
i32 sum(i31, i31){. . .}  // надёжная
i32 sum(i32, i32){. . .} // ненадёжная
А в вашем примере первый аргумент имеет тип i3, а второй — i4. Надо подумать. Но избежать лишних проверок, вероятно, лучше всего с помощью системы типов. Ну и тема с переполнениями требует отдельного описания, которой всё равно собирался заняться.

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

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

     2019/01/28 16:29, Автор сайта          # 

Дичайший аргумент. Может быть такая обязаловка потому что это ПРАВИЛО языка?

Вот видите, это Вам показалось дикостью. Потому что давно привыкли к этому и это давно стало правилом. А до появления Фортрана это не было правилом. Так вот обязательность проверки ошибки тоже должна стать правилом. И к этому можно привыкнуть, как к «then» после «if».

Если же лично я складываю а + б, я не буду его проверять на ошибки. Потому что если туда не поместится результат числа, то проблема не в РЕАЛИЗАЦИИ, а в ПРОЕКТЕ.

ТЗ содержит задание: посчитать среднюю сумму чека, тип double. ТЗ не разжёвывает Вам реализацию. Ваша реализация: первая функция вычисляет сумму всех чеков (тип double), вторая — количество всех чеков (тип unsigned int). Т.е. Вы лично складываете а + б и не проверяете на ошибки. Потом первое делиться на второе — и вот вам результат.

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

Операция деления САМА генерирует исключение... Ну так в чем проблема с которой Вы боретесь?

А в примере выше переполнение не генерирует исключение. Вот она проблема, с которой я борюсь: в одном случае реагируют на ошибки, а в другом нет! Как же найти единое решение? Да я его предложил.

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

Два момента. 1) Какой ценой? Я предложил минимальнейшую цену. 2) Установить источник могут, но не устанавливают! Почитайте блог производителя PVS Studio.

в JavaScript делить на нуль можно

Не приводите этот язык в пример, тут имеет место быть другая философия.

Не понятно, почему Ваша система будет лучше имеющихся решений.

Ладно, я за Вас свечку поставлю :) О вразумлении раба Божьего... Как звать-то Вас при крещении?

     2019/01/28 16:42, utkin          # 

Хороший вопрос. Честно говоря, дальше того, что описано выше, пока не думал, но вопрос про «многоэтажность» подталкивает к этому.

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

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

И я об этом говорил, не разжевывая мелочей, потому что думал, что Вам понятно, о чём речь. Отсюда и претензии. И этот вопрос остро встает в вопросе — как пользовательская функция понимает, что она допустит ошибку? У неё 30 строк кода и 80% из них опасны для общества (расчеты с участием деления, например). Что должен делать программист? Рвать волосы на ж..е? Что должна делать функция? Что должна делать функция, которая вызывает данную функцию? Что должна делать рекурсия?

Как тогда выглядит такая «транзитная» функция? Ключевое слово «ненадёжная» остаётся при ней, ветвь обработки ошибок («при ошибке») исчезает из кода на ЯВУ, но остаётся в неявном виде в сгенерированном ею машинном коде.

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

Ещё раз скажу: Вы задаёте хорошие вопросы.

А как же я :)? Рекурсия, мужик! Я не спрашивал так, исходя из наших прошлых бесед, был уверен, что Вам это и так ясно :).

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

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

Вот видите, это Вам показалось дикостью. Потому что давно привыкли к этому и это давно стало правилом. А до появления Фортрана это не было правилом. Так вот обязательность проверки ошибки тоже должна стать правилом. И к этому можно привыкнуть, как к «then» после «if»

Сейчас это всем фиолетово. Есть стандарт. Хотите новое, без проблем, но без торговой марки if. И это даже не мой личный каприз.

ТЗ содержит задание: посчитать среднюю сумму чека, тип double. ТЗ не разжёвывает Вам реализацию.

Значит это проблема проектировщика, а не Ваша. И всё, приплыли. А Ваша личная проблема в том, что Вы решаете за проектировщика как быть дальше.

Ваша реализация: первая функция вычисляет сумму всех чеков (тип double), вторая — количество всех чеков (тип unsigned int). Т.е. Вы лично складываете а + б и не проверяете на ошибки. Потом первое делиться на второе — и вот вам результат.

И? Это проблема ПРОЕКТИРОВЩИКА, он должен дать АЛГОРИТМ, диаграмму классов, блок-схему — всё, что угодно, это его проблема. Он же во время управления проектом должен отслеживать проблему. Программист здесь низшее звено (ниже только проектировщик интерфейса) и это НЕ ЕГО ПРОБЛЕМЫ ни в одном месте.

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

И что? А ещё человеку свойственно с..ть где не надо и убивать друг друга. Смысл специалиста заключается в том, что он решает СВОЮ проблему и поставленную ПЕРЕД НИМ ЗАДАЧУ и не лезет в проблемы других. У Вас есть проект и именно ему Вы ОБЯЗАНЫ следовать. Вы можете сказать менеджеру проекта, что есть косяк, но не имеете ни малейшего права его исправлять. Поверьте, в командной работе это ИМЕННО ТАК и никак иначе.

Вот она проблема, с которой я борюсь: в одном случае реагируют на ошибки, а в другом нет! Как же найти единое решение? Да я его предложил.

Еще раз, когда программист САМ делает 2 + 2 — это ЕГО ЛИЧНЫЕ ПРОБЛЕМЫ, а не проблемы компилятора. Когда программист занимается МУТНЫМ делом, которые от него не зависят ни с какого бока (чтение с файла, работа с сетью или БД) — это работа НЕЗАВИСИМЫХ АГЕНТОВ (есть теория на эту тему), и тогда их решения нельзя принимать на веру. Чувствуете разницу — складываю Я и складывает какой-то там чувак с Китая?

1) Какой ценой? Я предложил минимальнейшую цену.

Это вообще не факт.

2) Установить источник могут, но не устанавливают!

Мне класть на них! Я беру в Паскале и получаю номер строки с ошибкой. Открываю текст и вижу оператор (потому что, я такой, я пишу один оператор в строке, а не маюсь фигней). И вообще критические секции (типа чтения файла) всегда обернуты в бок try, а другие ошибки — это МОИ ОШИБКИ и они не должны обрабатываться в runtime.

Не приводите этот язык в пример, тут имеет место быть другая философия

И что? Это значит, что JavaScript'a нет вообще? Да у Вас другая логика, но она логика, пока может предъявить аргументы в свою защиту. А отмазка типа "Мы не такие" — это не защита, а бегство от аргументов.

Как звать-то Вас при крещении?

А я нехристь :) и Ваши штучки на меня не работают. Потому что в моем далеком детстве меня учили — выбрось на садовый овощ свои личные ощущения, верь только приборам.

     2019/01/28 18:24, Автор сайта          # 

о разнице между тем, что есть и о тормозах, которые будет порождать Ваша система обработки ошибок.

Я описал в ассемблерных кодах, почему исключения в C++ тяжеловесны. Описал, какие машинные коды генерирует моё предложение. Я свои слова подкрепляю какими-то фактами, а Вы — нет.

как пользовательская функция понимает, что она допустит ошибку?

ОС вернёт код ошибки, когда файл не читается; системную функцию чтения надо «обернуть» собственной функцией чтения со своей обработкой ошибок. Когда вы преобразуете строку в число и встречаете там букву, то сами принимаете решение: это ошибка.

Что должна делать рекурсия?

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

Вы часто даёте ссылки на разные ресурсы. В основном, конечно, мусор

Умеете Вы льстить.

Но вот я дал ссылку на какой-то такой механизм в стандарте С++ 14. Чего бы не посмотреть, как люди до Вас взяли и изобрели колесо?

Всё руки не доходят. Сижу целыми днями, Уткину отвечаю. А как освобожусь — обязательно!

Вы задаёте хорошие вопросы.

А как же я :)? Рекурсия, мужик! Я не спрашивал так, исходя из наших прошлых бесед, был уверен, что Вам это и так ясно :).

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

Как это компилятор сможет предсказать?

Компилятор, встретив деление, остаток от деления, сдвиг вправо, где второй аргумент — константа, может предсказать.

Есть стандарт. Хотите новое, без проблем, но без торговой марки if. И это даже не мой личный каприз.

Моя торговая марка — слово «ошибка», слова «исключение», если Вы заметили, предпочитал не употреблять.

Это вообще не факт.

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

это МОИ ОШИБКИ и они не должны обрабатываться в runtime.

Не переживайте, никто не лишает Вас Ваших ошибок, ведь они Вам так дороги.

отмазка типа "Мы не такие" — это не защита, а бегство от аргументов.

Я ж написал в «Философии», что цель — создание языка, пригодного для системного программирования. JS таковым не является, поэтому приводить его в пример не стоит. В нём применяются «дорогие» решения.

     2019/01/28 19:06, Comdiv          # 

 ошибка = . . .
Что будет происходить после этого? Поток исполнения в функции продолжит свой ход или сразу перейдёт к обработчику на верхнем уровне?

атрибут ненадёжности не является обязательным, от него можно избавиться

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

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

На каждый случай типов не напасёшься. Рассмотрите ещё и такой случай:
if (f) {
a = rand();
b = -rand();
} else {
a = a1 % 43;
b = b1 / 1024;
}
sum = add(a, b)

А с чем связан Ваш вопрос насчёт отделения ошибок подпрограмм и ошибок данных?

Многие системы обработки исключений смешивают обработку программных ошибок и ошибок входных данных, что некорректно во многих случаях. Рассмотрим ещё один пример:
try {
a = add(inputA, inputB);
p = mul(a % 8, inputB /* % 8 ошибка в программе*/);
log(p);
} catch {
log("недопустимые входные данные")
}
Зная, что inputA и inputB получены из ненадёжного источника, программист помещает их сложение в блок с ловлей исключений, чтобы иметь возможность сообщить о некрректных данных. В тот же блок помещается второе действие, которое должно быть выполнено в случае успешного 1-го и которое по проекту обязано выполниться без переполнения. Но из-за ошибки в коде, в ней тоже может произойти исключение, что приведёт к выполнению обработчика некорректных данных, что не то, что нужно.

     2019/01/29 07:46, utkin          # 

Я описал в ассемблерных кодах, почему исключения в C++ тяжеловесны. Описал, какие машинные коды генерирует моё предложение. Я свои слова подкрепляю какими-то фактами, а Вы — нет.

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

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

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

Умеете Вы льстить

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

     2019/01/29 16:21, Автор сайта          # 

2 Comdiv

Что будет происходить после этого?

Функция досрочно завершит свою работу: возвращаемым значением становится характеристика ошибки, затем делается возврат из функции в вызвавшую функцию. Возврат делается не в точку вызова, а в точку обработки ошибки (ветвь «при ошибке»).

На каждый случай типов не напасёшься.

Если функция rand выдаёт значение типа i31 и функция sum описана как i31 sum(i31, i31), то да, возможны переполнения. Суммирование чисел повышает требование к разрядности на 1 бит. Если в цикле делается 1000 сложений, то результат должен храниться в N+1000 разрядах, где N – исходная разрядность операндов. Да, Вы правы, аппаратная целочисленная арифметика не предоставит столько разрядов. Если оставаться в рамках предоставленной компьютером разрядности, то моё предложение об обработке ошибок как раз и пригодится. Или надо переходить на программную реализацию больших чисел.

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

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

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

Функция mul отработает без переполнений, если у inputB три старших бита — нули. Если это так, то можно переписать Ваш код:
try {
a = add(inputA, inputB);
} catch {
log("недопустимые входные данные")
}
p = mul(a % 8, inputB /* % 8 ошибка в программе*/);
log(p);
2 utkin

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

Сначала я пишу, что моё решение лекговесно, а в C++ — тяжеловесно. Это и есть мой аргумент, чем оно лучше. Вы пишите, что у меня будет тормозить (т. е. всё наоборот, у меня тяжеловесно). Я опровергаю, привожу в доказательство ассемблерный код. Вы опять пишите, что будет тормозить. Я пишу: а где Ваш ассемблерный код, покажите и докажите. Вы отвечаете, что сначала нужно понять, чем оно лучше остального. Да тем, что оно легковеснее! И я уже это доказывал.

Дискуссия, однако.

Почему я?

Ладно, не Вы. Убедили, я сдаюсь. Ведь как Вы пишите свой язык «Валентина»? Вам разжевали, разложили по полочкам, ТЗ написали. Да такое подробное, что там даже есть описание преобразования строки в число. Не знаю, кто Ваш благодетель, но жму ему руку.

     2019/01/29 16:58, MihalNik          # 

Суммирование чисел повышает требование к разрядности на 1 бит. Если в цикле делается 1000 сложений, то результат должен храниться в N+1000 разрядах, где N – исходная разрядность операндов.

N+10

     2019/01/29 18:21, Comdiv          # 

Функция досрочно завершит свою работу ... возврат делается не в точку вызова, а в точку обработки ошибки.

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

В таком варианте ошибки подпрограмм и ошибки данных неотделимы

Можете описать поподробней с примером?

можно переписать Ваш код:

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

Вот, к примеру, тот же код с обычным способом обработки:
if (add(&a, inputA, inputB)) {
p = mul(a % 8, inputB /* % 8 ошибка в программе*/);
log(p);
} else {
log("недопустимые входные данные")
}

     2019/01/29 18:26, Comdiv          # 

Забыл добавить, что в моём примере сложение и умножение были вынесены в функции и разделены по операторам для наглядности. В языке с исключениями это могло бы выглядеть так:
try {
p = (inputA + inputB) % 8 * (inputB/**/);
log(p);
} catch {
log("недопустимые входные данные")
}

     2019/01/30 10:33, Автор сайта          # 

N+10

И в правду, Вы правы. Ошибся я. Для 210 сложений разрядность должна увеличиться на 10.

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

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

На уровне машинных инструкций разница в том, что благодаря «дешевизне» средств это встаёт на один уровень с «if». Такая обработка ошибок — ещё одна разновидность условного оператора. Однако в C++, насколько мы знаем, «try» не используют и в хвост, и в гриву. Это моветон. Это инструмент не повседневного, а исключительного использования. Всё по причине тяжеловесности. Легковесность предложенного сравнима с Rust, но ещё легче, потому что в Rust типы Option и Result не могут быть помещены в регистр при возврате значения. Там и машинных инструкций больше, и дополнительная память требуется.

На уровне синтаксиса предложенная конструкция легче тем, что отсутствует аналог «try». Он как бы подразумевается. За это придётся расплачиваться дополнительным ключевым словом «ненадёжная» в определении функции. Но это даёт выигрыш в семантике.

Семантика же предложенного способа обязывает делать проверки. Вот Вы привели

код с обычным способом обработки

А ниже — аналогичный с «try». Но и то, и другое — добровольная проверка, а не необходимость, продиктованная синтаксисом. А в предложенном способе проверка ни при каких обстоятельствах не может быть забыта. Ну как не может быть забыта ветвь «then» после «if».

Да, кусочки решения есть во многих языках: C++, Rust, Haskell. Тянет ли на нововведение сумма кусочков, их комбинация — потом будет видно.

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

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

с учётом "многоэтажности", где уже неясно где находится обработчик.

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

Можете описать поподробней с примером?

На смеси нижегородского с французским:
учётная запись  ненадёжная импорт учётной записи (...) {
( учётная запись . id = читать id ()
учётная запись . ФИО = читать ФИО ()
учётная запись . email = читать email ()
учётная запись . полномочия = читать полномочия ()
при ошибке
ошибка = причина (НЕ ПРОШЛА ПРОВЕРКУ)
}
. . .
( моя учётная запись = импорт учётной записи (. . .)
при ошибке
Сообщение("ошибка чтения учётной записи"))
Примерно так.

     2019/02/05 22:14, utkin          # 

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

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

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

Непонятно почему Вы так решили. Нелюбимым занятием по сравнению с чем?
( ...
m * n + p / q - r
при ошибке
// реакция на ошибки в цепочке вычислений
)
Деление у нас всегда ненадежная операция, верно? Это значит, что ВЕЗДЕ, где будет деление, программист будет обязан писать обработчик ошибок? Вам не кажется, что в таком случае программисты будут тупо забивать и ставить там заглушки?

     2019/02/07 19:01, Автор сайта          # 

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

На форумах можно встретить немало жалоб на второе, но никак на первое.

Непонятно почему Вы так решили. Нелюбимым занятием по сравнению с чем?

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

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

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

     2019/02/07 19:25, utkin          # 

На форумах можно встретить немало жалоб на второе, но никак на первое.

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

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

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

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

Вы не ответили на вопрос — теперь в каждой функции, где есть деление требуется такой обработчик? Вы говорите здесь о причинах механизма, мне сейчас хочется понять как это будет выглядеть практически. Я к тому веду, что try я пишу там, где он НУЖЕН. А здесь? Я буду писать дополнительный код всегда? И там где он нужен и там, где он не нужен? Просто потому, что существует вероятность возникновения ошибки?

     2019/02/08 16:46, Автор сайта          # 

Вы ремесленник

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

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

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

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

Так что да, подтверждаю: проверки обязательны везде просто потому, что существует вероятность нечаянно взорвать ядерный реактор. Вот примеры из жизни, к чему приводят ошибки в программах. Шведская биржа была парализована лотом на покупку -6 фьючерсов: число -6 было истолковано как 4294967290. Стоимость получившегося лота в 131 раз превысила ВВП Швеции. В другом случае француженка получила счет на 12 квадриллионов евро. А вот небезобидный случай: авария ракеты «Ариан-5» из-за ошибки в программе системы инерциальной ориентации. Некорректное преобразование 64-битного числа с плавающей точкой в 16-битное целое со знаком принесло ущерб в сотни миллионов долларов.

     2019/02/09 08:21, utkin          # 

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

Нет, что Вы операционные системы пишут художники конечно. Вы серьезно? Вы знаете почему Java один из самых часто используемых? Узнайте.

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

И при этом Вы говорите об эффективности? Ваше решение в цикле это — адские тормоза. Возьмите факториал: даже в данном сферическом коне в вакууме, если там есть ОБЯЗАТЕЛЬНАЯ проверка, она приведет к тормозам в сравнении с обычным С++. Просто потому что там нет медленного и ужасного try, а в Вашем случае Ваш супербыстрый код будет вызываться каждый раз, когда он НЕ НУЖЕН в алгоритме.

Вот примеры из жизни, к чему приводят ошибки в программах.

И Вы их решить подобным образом НЕ СМОЖЕТЕ.

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

Ещё раз — Вы переносите ПРОЕКТИРОВАНИЕ в ПРОГРАММИРОВАНИЕ. От этого процесса отказались с того момента, когда информационные системы стали очень большими. Есть такая вещь — ПРОГРАММНАЯ ИНЖЕНЕРИЯ. Читайте её! Для Вас программист — это творец. А в мировой экономике программист — это рабочая сила. Программа должна быть изначально спроектирована. И если она СПРОЕКТИРОВАНА, то Ваш механизм там НЕ НУЖЕН. Вы это понимаете? try используется там и ТОЛЬКО ТАМ, где он нужен.

     2019/02/09 15:16, Автор сайта          # 

Нет, что Вы операционные системы пишут художники конечно. Вы серьезно? Вы знаете почему Java один из самых часто используемых? Узнайте.

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

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

Ваше решение в цикле — это адские тормоза.

Приводите факты! Д.Ю. Караваев, в компиляторе которого проверки и в цикле, и вне цикла, заявляет о замедлении в доли процента. В моём предложении будет такое обременение: пара машинных инструкций при вызове подпрограммы и пара инструкций при выходе из неё. Если Вы хотите и дальше утверждать о тормозах, приводите в следующий раз доказательства.

Возьмите факториал

Рекурсивные вызовы по определению проигрывают циклам.

Ваш супербыстрый код будет вызываться каждый раз, когда он НЕ НУЖЕН в алгоритме.

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

Вы их решить подобным образом НЕ СМОЖЕТЕ.

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

Есть такая вещь — ПРОГРАММНАЯ ИНЖЕНЕРИЯ.

А это наука или религия? Если религия, то конечно, с одиннадцатой заповедью всунуться некуда. Но поскольку это наука (и при этом — наука-надстройка над другими науками), то ей свойственно адаптироваться к смежным наукам. Постулаты программной инженерии приспосабливаются. То к ООП, то к шаблонам, то функциональному программированию. Ведь смежные науки (караул!!!) развиваются.

В моём предложении обязательность проверок во многом аналогична обязательности в Haskell. Как-то программная инженерия смогла же переварить фреймворк Yesod, написанный на Haskell?

     2019/02/24 00:10, rst256          # 

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

Кто-то будет следовать данному правилу, но что даст например код:
y = (tan(x) при ошибке 0)   // ?
Мы обезопасили себя от того, что программа вылетит с ошибкой. От того, что х примет недопустимое для аргумента функции тангенса значение. Наша программа будет работать с возможно засевшей где-то в ней ошибкой, так как будто она исправная, можно быть уверенным, что в конкретно данной строчке кода всё будет идти так, как будто значение х правильное. Но если это будет не так, мы об этом даже не узнаем. Но ошибка как была, так и осталась, самая страшная ошибка — логическая. И мы только что успешно лишили себя возможности своевременно о ней узнать.

В некоторых случаях такое правило может вызвать логические ошибки в мозгу самого программиста:
y = (tan(pi) при ошибке 0)
В коде, где невозможно появление ошибки в принципе, нужно задать реакцию на ошибку!

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

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

     2019/02/24 11:00, Автор сайта          # 

если она [функция] ненадёжная, тогда какого лешего она делает в релизе?

Да у нас масса функций ненадёжна, просто у нас глаз замылился и мы перестали это замечать. Что, к примеру, делают в нашем коде функции ввода/вывода, которые всегда приносят риск, что они не будут выполнены? Почему ввод/вывод ненадёжен, какого лешего он попал в релиз?! Да просто возможность ошибки — это характерная черта этих функций. Сделать их надёжными невозможно хотя бы потому, что файл, который мы хотим прочитать, может просто не существовать. Или хотим записать файл, но права на запись по каким-то причинам отсутствуют. Как мне сделать функцию чтения файла надёжной, чтоб всегда выдавала содержимое файла? Аварийно завершить программу, выдав сообщение? А может логика программы позволяет обойтись без содержимого этого файла?

А если функция преобразования строки в число находит в строке букву, она должна аварийно завершить программу? Да нет, ей лучше вернуть признак ошибки, а что с этим делать — пусть решает программист. Но он обязательно должен обратить на это внимание.
Код
y = (tan(x) при ошибке 0)
плох тем, что 0 входит в область значений tan(x). Получается, что «y» может иметь значение 0 как вследствие нормальной работы tan(x), так и вследствие ошибки. Область значений функции tan(x) — от минус бесконечности до плюс бесконечности. Поэтому какое значение ни указать после «при ошибке», всё равно оно неотличимо от значения, выданного tan(x). Здесь надо либо аварийно завершать программу, либо менять логику работу программы — неясно, что хотели сделать со значением тангенса.

Если оставить код такой, как Вы указали, то это логическая ошибка. Логические ошибки находить труднее всего.

В коде, где невозможно появление ошибки в принципе, нужно задать реакцию на ошибку!

Функции, в принципе не допускающие ошибок, не будут помечаться атрибутом «ненадёжная». И тогда ветка «при ошибке» вызывает ошибку при компиляции: «бесполезная ветвь обработки ошибок».

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

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

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

Владимир Патрышев когда-то вёл блог и писал о монадах в Scala. Его спрашивают в комментариях: «А может не нужна обязательность проверок? Допустим, написали код, проверили его и отладили. Убедились, что работает. И поле этого убрали проверки (чтоб работало быстрее) и отдали программу в эксплуатацию». На что Владимир отвечает: «Значит, когда идут испытания самолёта, мы его обвешиваем телеметрией, всякими чёрными ящиками, предохранителями и прочей защитой от дурака. А когда самолёт вылетает с пассажирами, мы это всё это снимаем? Летите, граждане пассажиры, без предохранителей!».

     2019/03/13 15:49, Бурановский дедушка          # 

Серия недавних катастроф с новой моделью «Боинга» с очень большой вероятностью связана с ошибками в ПО. Так что тема обработки ошибок поднята правильно. Можно придраться к деталям реализации, но принцип обязательности проверки — это движение в верном направлении.

     2019/03/15 18:20, Автор сайта          # 

Да, уместно Вы вспомнили про катастрофу. Я даже Вас почти процитировал, говоря об ошибках, выявляемых в runtime:

Жаль, конечно, что ошибки Boeing 737 MAX 8 выявляются только fly time.

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

     2019/03/16 19:26, Бурановский дедушка          # 

Любопытно, предъявят разработчикам ПО «Боинга» статью «непреднамеренное убийство»? Тут сослаться на «as is» не получится.

     2019/03/16 22:53, kt          # 

Скорее, это не ошибка в ПО в "программистcком" понимании, а ошибка в ТЗ. Вероятнее всего, ПО написано так, как этого требует ТЗ.

     2019/03/18 01:08, Автор сайта          # 

После регулярного чтения отчётов об ошибках, которые обнаружил PVS-Studio, я теперь вообще не верю, что существуют программы без ошибок. Какое-то время были иллюзии, связанные с языками, дружественные к доказательству правильности программ. Но и они были развеяны, когда задал вопрос знатоку Haskell: «А как контролируется переполнения в Haskell?», на что получил ответ, что никак.

А в ТЗ так же наверняка есть ошибки. Даже если эфиопская катастрофа не связана с ошибками в ПО, то это не означает безошибочность ПО и ТЗ :)

     2019/03/20 14:58, rst256          # 

А какая реакция на ошибку должна быть предусмотрена?

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

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

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

     2019/03/20 16:13, rst256          # 

Серия недавних катастроф с новой моделью «Боинга» с очень большой вероятностью связана с ошибками в ПО. Так что тема обработки ошибок поднята правильно. Можно придраться к деталям реализации, но принцип обязательности проверки — это движение в верном направлении.

Вот в ПО «Боинга» я бы хотел иметь именно стандартный обработчик всех ошибок, который не пытается исправить их во флайтайм, а сразу же вырубает автопилот к чертям собачим. И подает сигнал об отключении автопилота летчикам очень громким звуком и ярчайшей мигалкой, что бы никаких шансов не заметить его просто не было. Раз в программе возникла ошибка, значит она СТАЛА НЕНАДЕЖНОЙ. Пилот там что — для галочки сидит? Или у них теперь пилоты без ЭВМ-а летать разучились? Повторюсь, при ошибке вырубать ВСЕ завязанные на ЭВМ функции. Все приборы, показазания которых поступаю с ЭВМ, при этом должны быть закрыты шторками, на которых большими красными буквами написано "НЕ РАБОТАЕТ". Полный комплект приборов и органов управления позволяющих вести машину без помощи ЭВМ — обязателен. Как и наличие летчиков способных на это. Я бы ещё их прежде, чем в кабину допустить, заставил сдать все нормативы по полетам на планерах и АН-2.

     2019/03/26 16:53, Автор сайта          # 

Статья Джоэла Спольски, хоть и устарела (написана аж 2003 году), но не устарели причины, по которым она написана.

Исключения

Люди спрашивают, почему я не люблю программировать с исключениями. В Java и C ++ моя политика такова:
  • сам никогда не бросай исключений,
  • всегда перехватывайте любое возможное исключение, которое может быть вызвано библиотекой, которую я использую, в той же строке, в которой оно выдается, и немедленно устраняйте её.
Причина заключается в том, что я считаю исключения не лучше, чем «goto», считающиеся вредными с 1960-х годов, в том смысле, что они создают резкий переход от одной точки кода к другой. На самом деле они значительно хуже, чем «goto»:
  • Они невидимы в исходном коде. Глядя на блок кода, включая функции, которые могут генерировать или не генерировать исключения, нет никакого способа увидеть, какие исключения могут быть выброшены и откуда. Это означает, что даже тщательная проверка кода не выявляет потенциальных ошибок.
  • Они создают слишком много возможных точек выхода для функции. Чтобы написать правильный код, вам действительно нужно продумать каждый возможный маршрут кода в вашей функции. Каждый раз, когда вы вызываете функцию, которая может вызвать исключение и не перехватить его по месту, вы создаете возможности для неожиданных ошибок, вызванных внезапно завершившимися функциями, оставив данные в несогласованном состоянии или других маршрутах выполнения кода, о которых вы не подумали.
Лучшая альтернатива состоит в том, чтобы ваши функции возвращали значения ошибок, когда что-то идет не так, и работали с ними явно, независимо от того, насколько многословно это могло бы быть. Это правда, что то, что должно быть простой трехстрочной программой, часто вырастает до 48 строк, когда вы добавляете хорошую проверку ошибок. Но это жизнь, и её обработка с исключениями не делает вашу программу более устойчивой. Я думаю, что причина, по которой программистов на языках стилей C / C ++ / Java привлекают исключения, заключается в том, что у синтаксиса нет краткого способа вызова функции, которая возвращает несколько значений, поэтому трудно написать функцию, которая либо возвращает значение, либо возвращает ошибку. (Единственные языки, которые я широко использовал, которые позволяют вам правильно возвращать несколько значений, - это ML и Haskell.) В языках стилей C / C++ / Java одним из способов обработки ошибок является использование реального возвращаемого значения для статуса результата. Для возвращения статуса результата надо использовать параметр OUT. Это имеет нежелательный побочный эффект, который делает невозможным вложение вызовов функций, поэтому result = f (g (x)) должно стать:
T tmp;
if (ERROR == g(x, tmp))
     errorhandling;
if (ERROR == f(tmp, result))
     errorhandling;
Это уродливо и раздражающе, но это лучше, чем разбрасывать волшебные неожиданные сообщения по всему вашему коду в непредсказуемых местах.

     2019/03/26 23:07, Автор сайта          # 

Статья на Хабре: «Исключения в Python теперь считаются анти-паттерном». Попытка сделать обработку ошибок в Python лучше. Подходы традиционны, т. е. нового ничего не содержат. Любопытны комментарии:

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

Ощущение, что чуваки увидели Rust и поняли, что до этого занимались какой-то фигней :)

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

Не так-то просто в старые языки запихнуть новые концепции.

Добавить свой отзыв

Написать автору можно на электронную почту mail(аt)compiler.su

Авторизация

Регистрация

Выслать пароль

Карта сайта


Каким должен быть язык программирования?

Анализ и критика

Устарел ли текст как форма представления программы

Русский язык и программирование

Многоязыковое программирование

Синтаксис языков программирования

Синтаксический сахар

Некоторые «вкусности» Алгол-68

«Двухмерный» синтаксис Python

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

Должна ли программа быть удобочитаемой?

Стиль языка программирования

Тексто-графическое представление программы

●  Разделители

●  Строки программы

●  Слева направо или справа налево?

Комментарии

●  Длинные комментарии

●  Короткие комментарии

●  Комментарии автоматической генерации документации

●  Нерабочий код

●  Помеченные комментарии

Нужны ли беззнаковые целые?

Шестнадцатиричные и двоичные константы

Условные операторы

Переключатель

Циклы

●  Продолжение цикла и выход из него

Некошерный «goto»

Изменение приоритетов операций

Операции присвоения и проверки на равенство. Возможно ли одинаковое обозначение?

Так ли нужны операции «&&», «||» и «^^»?

Постфиксные инкремент и декремент

Почему в PHP для конкатенации строк используется «.»?

Указатели и ссылки в C++

●  Обработка ошибок

Использование памяти

Почему динамическое распределение памяти — это плохо

Как обеспечить возврат функциями объектов переменной длины?

●  Типы переменного размера (dynamically sized types, DST) в языке Rust

●  Массивы переменной длины в C/C++

●  Размещение объектов в стеке, традиционный подход

●  Размещение объектов переменной длины с использованием множества стеков

●  Размещение объектов переменной длины с использованием двух стеков

●  Реализация двухстековой модели размещения данных

●  Двухстековая модель: тесты на скорость

●  Изменение длины объекта в стеке во время исполнения

●  Размещение объектов переменной длины с использованием одного стека

Можно ли забыть о «куче», если объекты переменной длины хранить в стеке

Безопасность и размещение объектов переменной длины в стеке

Массивы, структуры, типы, классы переменной длины

О хранении данных в стеке, вместо заключения

Реализация параметрического полиморфизма

Описание языка

Компилятор

Отечественные разработки

Cтатьи на компьютерные темы

Компьютерный юмор

Новости и прочее

Последние комментарии

2019/04/19 11:25 ••• kt
Почему языки с синтаксисом Си популярнее языков с синтаксисом Паскаля?

2019/04/14 22:15 ••• MihalNik
К вопросу о совершенствовании языка программирования

2019/04/03 22:24 ••• Антон
Все голосования

2019/04/02 12:28 ••• Автор сайта
Шестнадцатиричные и двоичные константы

2019/04/02 12:25 ••• Автор сайта
Выбор кодировки для компилятора

2019/03/26 23:07 ••• Автор сайта
Обработка ошибок

2019/03/24 14:55 ••• Автор сайта
Реализация двухстековой модели размещения данных

2019/03/23 19:01 ••• Автор сайта
Размещение объектов переменной длины с использованием множества стеков

2019/03/20 14:37 ••• rst256
Реализация параметрического полиморфизма

2019/02/24 23:14 ••• MihalNik
Каким должен быть язык программирования?

2019/02/20 14:09 ••• Автор сайта
Ошибка при отсутствии выполняемых действий

2019/02/16 14:52 ••• kt
Заметки о выходе из функции без значения и зеркальности get и put