«Ошибки — это значения» в Go и эхо VB

Судьба завела меня (программиста практика, в основном использующего C#) на проект, в котором основной функционал разрабатывается на Go.

Изучая Go обратил внимание на непривычную практику обработки ошибок. Почитав разъяснения в статьях Ошибки — это значения и в Почему «ошибки это значения» в Go отметил, что предлагаемые там решения заставляют вспомнить одну особенность Visual Basic, которую очень не лестно комментировали программисты.

Суть в следующем. Существуют жалобы программистов про проверку ошибок в Go. Возьмём пример из статьи Ошибки — это значения:

_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on

Мы видим, что присутствует повторяющий код, связанный с обработкой ошибок, который и вызывает нарекания. Вроде бы, почему бы не использовать стандартную практику try-catch и ошибка будет обрабатываться в одном месте?

Далее в статье Ошибки — это значения предлагается решение:

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

w := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}


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

И тут, у меня возникло ощущение, что что-то подобное в истории было. И было это в Visual Basic и связано это было с оператором If. Буду говорить о Visual Basic в прошедшем времени, так как давно не работал с ним.

В Visual Basic присутствовал оператор If и в отличии от C++ и Java отсутствовал оператор ?:. Таким образом, записывался такой код:

Dim a As Integer
If CheckState() Then
	a = 12
Else
	a = 13
End If

Со стороны VB программистов были жалобы: много повторяющегося кода, хорошо бы его сократить до одной строки и иметь возможность использовать inline. Так, возможно, появился IIf:

a = IIf(CheckState(), 12, 13)

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

a = IIf(CheckState(), GetTrueAValue(), GetFalseAValue())

И для многих программистов было неожиданностью то, что в данном коде будут вызваны обе функции: GetTrueAValue и GetFalseAValue. Хотя они ожидали, что IIf будет работать, как оператор If (или как ?: в С++), т.е. они не понимали, какой смысл вызывать вторую функцию, когда её результирующее значение не имеет смысла.

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

Суть оператор If в Visual Basic (да и в других языках) определять участки кода, которые должны выполняться в зависимости от условия. Попытка использовать IIf дало лишь поверхностное решение и не оправдало основного ожидания от неё – вызывать ту или иную функцию в зависимости от условия.

Так вот. Вернёмся к Go и к практике, которую предлагают для работы с ошибками. У меня, сложилось такое впечатление, что выше приведённая практика обработки ошибок очень напоминает IIf в Visual Basic. Расширим пример:

w := &errWriter{w: fd}
ew.write(getAB())
ew.write(getCD())
ew.write(getEF())
// and so on
if ew.err != nil {
    return ew.err
}

Соответственно, здесь возникает вопрос: если в строке ew.write(getAB()) возникает ошибка, то почему выполняются getCD() и getEF(), когда в их результирующих значениях смысла нет?

Почему возникает такой вопрос? Дело в том, что ошибка — это значение, которое определяет, какой код должен выполняться. И это четко видно в первоначальном способе обработки ошибок в Go.

_, err = fd.Write(getAB())
if err != nil {
    return err
}
_, err = fd.Write(getCD())
if err != nil {
    return err
}
_, err = fd.Write(getEF())
if err != nil {
    return err
}

Значение ошибки обрабатывается оператором if. И обработка значения ошибки в if как раз и определяет, какой код должен быть выполнен дальше. При чём в большинстве случаев для принятия решения, важно наличие ошибки, а не её содержания.

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

P.S.: Тут вспомнилось из VB:

On Error Resume Next

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

Подробнее
Реклама
Комментарии 166
  • –10
    Пример в статье был подан не как «вот вам специальный IIf, чтобы хендлить вот такой случай», а как пример того, как смотреть на ошибки. Если понять посыл статьи, то всё становится намного проще.
    В аналогиях важно видеть не только схожее, но и различное.
    • +10
      Это не отвечает на вопрос «если в строке ew.write(getAB()) возникает ошибка, то почему выполняются getCD() и getEF(), когда в их результирующих значениях смысла нет?»

      Кстати, на этом примере как раз хорошо видна разница между го-шным «ошибки — это значения» и монадой Try, которая тоже трактует ошибки как значения.
      • –9
        +3 статьи и перевода по Go :)
        • –6
          Это не отвечает на вопрос «если в строке ew.write(getAB()) возникает ошибка, то почему выполняются getCD() и getEF(), когда в их результирующих значениях смысла нет?»

          Представьте, что речь не про ошибки, и ответьте сами на этот вопрос :)
          • +7
            А во всех случаях ответ один и тот же — потому что код написан неэффективно. Так зачем же предлагать заведомо неэффективное решение?

            (кстати, это еще и не рефакторинг, вопреки тому, что написано в оригинальной статье)
            • +2
              Хотел уточнить, эффективно это ty-catch-finally для control flow?
              • 0
                Зависит от реализации, очевидно. Может быть try-catch, может быть, композиция монад, может быть — тупая цепочка ифов.
                • +3
                  Насколько я знаю концепция исключений для управления состоянием, в общем случае, считается антипаттерном. К этому, в частности, привело то, что во многих ЯП, отношение к исключениям, сама логика их работы и реализация менялась со временем. И как я понимаю, изначально маленькая команда разработки Go сознательно отказалась от этой концепции учитывая предыдущий опыт.

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

                  Мне вот интересно, что осталось за бортом? Более практичное и функциональное, чем IF и менее сложное в реализации и отладке, нежели монады и исключения.

                  P.S. Последние несколько месяцев по работе имею доступ к одному большому проекту на C++, достаточно узкоспециализированному, но очень популярному в своей нише. Продукт очень недешевый, и позиционируется как надежный, но вот исключениями там совсем беда, и чем старее куски кода, тем все хуже. :(
                  • 0
                    изначально маленькая команда разработки Go сознательно отказалась от этой концепции учитывая предыдущий опыт.

                    В Google исключения не используются нигде, в том числе в С++, именно по озвученной вами причине.
                    • 0
                      Ой не надо. Используются. Как минимум в 2010м использовались.
                    • +2
                      Насколько я знаю концепция исключений для управления состоянием, в общем случае, считается антипаттерном.

                      Состоянием чего?

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

                      Ну вот в C# (и вообще в .net) отношение и логика не менялись никогда (насколько я знаю). Реализация внутри если и менялась, то это было инкапсулировано, и внешнее поведение оставалось неизменным.

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

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

                      Особенно учитывая, что делает (должна делать) это команда разработки самого языка (его базовой библиотеки), а никак не разработчики-пользователи.
                      • +1
                        Под состоянием я имел ввиду логику исполнения (control flow). С C# я работал достаточно давно, уж лет 8 назад, но тогда считалось, нерациональным использовать исключения для управления логикой из-за огромных накладных ресурсов и просадки производительности. Подозреваю, что сейчас это не так актуально, так как часто это встречаю в C# коде. В Java сама реализация исключений переписывалась, на моей памяти, как минимум два раза.

                        Собственно говоря, мой вопрос о чем-то посредине остался без ответа :( Тут ниже уже написали по макрос для Rust, интересная тема, но тут опять же, надо были изначально закладывать такую возможность.
                        • +5
                          Под состоянием я имел ввиду логику исполнения (control flow).

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

                          [в C#] считалось, нерациональным использовать исключения для управления логикой из-за огромных накладных ресурсов и просадки производительности.

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

                          Собственно говоря, мой вопрос о чем-то посредине остался без ответа

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

                          Тут ниже уже написали по макрос для Rust, интересная тема,

                          Эмм, макрос try! — это монада Try (в Rust она выражена типом Result) + синтаксический сахар (в скале — for-comprehension, в F# — computational expression). Вот вам приблизительно тот же код на F# (править под BCL не стал, извините):

                          fun fileDouble path =
                            Try {
                              let! file = File.open(path)
                              let! contents = file.readToString()
                              let! n = contents.trim() |> parse<int>
                              return (2 * n)
                            }
                          


                          (хотя, конечно, макросы выглядят красиво, не поспоришь)
                          • –3
                            Хорошо, а как тогда запретить использовать исключения во всех остальных случаях? Собственно, насколько я знаю, причины отсутствия исключений в Go, это сложность реализации и желание ограничить область использования исключительно ошибками.

                            Действительно, исключения в .net ресурсоемки — но это не значит, что невозможно сделать нересурсоемкую реализацию исключений.


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

                            Более того, вот вы пишите, что до сих пор это считается порочной практикой в C#, а на деле это происходит достаточно часто.

                            Насчет монад, уже вроде решили, что сделать на Go так нельзя. Мне вот так сходу трудно здесь сделать какие-то выводы, почему так сложилось исторически, надо дальше копать.
                            • 0
                              Хорошо, а как тогда запретить использовать исключения во всех остальных случаях?

                              Никак (ну, кроме статического анализа кода, да и то).

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

                              В Go есть panic, который ведет себя «почти как эксепшн» (есть некоторые весьма существенные отличия, но я не знаю, определяющие ли они для того, чтобы запретить все лишние возможности).

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

                              Это не пустячное дело, но это реализуемо. Основная проблема исключений не в ресурсоемкости, а в тех компромисах, на которые мы идем. И вот как раз они-то и служат поводом для дискуссий.

                              Более того, вот вы пишите, что до сих пор это считается порочной практикой в C#, а на деле это происходит достаточно часто.

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

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

                              Монады были приведены к вашему упоминанию Rust, решение которого вам нравится — а монады, почему-то, нет.
                              • 0
                                К монадам у меня вполне индифферентное отношение. Но без доп. информации я не могу сказать почему этот вариант проигнорировали.
                                • +1
                                  В смысле «почему его проигнорировали в Go»? Потому что адекватные монады нельзя сделать без дженериков, а дженериков в Go нет.
                                  • +1
                                    Понятно, что нельзя, я вот нарыл такой док, в свободное время прочитаю, потому что тема с дженериками постоянно всплывает, и опять же, верить на слово никому нельзя.
                            • –2
                              Так покрасивше всё ж:
                              fun fileDouble (path: string) {
                              return 2 * path.read.trim.parse!int
                              when FileNotFound return 0
                              }
                              • 0
                                Даже не буду спорить. Чем обеспечивается функциональность (я, к сожалению, на глаз язык не узнал).
                                • –2
                                  В данном случае это Go + D + [немного фантазии].
                              • +1
                                исключения в .net ресурсоемки — но это не значит, что невозможно сделать нересурсоемкую реализацию исключений.
                                В Mono у них производительность примерно на порядок выше, если мне не изменяет память. Так что да, возможно, причём сохраняя совместимость с CLR.
                              • –1
                                Ради интереса сделал тест для шарпа:
                                class Program
                                    {
                                        static void Main(string[] args)
                                        {
                                            for (int i = 0; i < 10; i++)
                                            {
                                                Test("Clean test #" + i.ToString(), Test1);
                                                Test("TryCatch   #" + i.ToString(), Test2);
                                                Console.WriteLine();
                                            }
                                            Console.ReadKey();
                                        }
                                
                                        static void Test(string name, Action action)
                                        {
                                            Stopwatch sw = new Stopwatch();
                                            sw.Start();
                                            action();
                                            sw.Stop();
                                            Console.WriteLine(String.Format("Test '{0}'. Elapsed time: {1}ms", name, sw.ElapsedMilliseconds));
                                        }
                                
                                        static void Test1()
                                        {
                                            long sum = 0;
                                            for (int i = 0; i < 100000000; i++)
                                            {
                                                sum += z(i);            
                                            }
                                        }
                                
                                        static void Test2()
                                        {
                                            long sum = 0;
                                            for (int i = 0; i < 100000000; i++)
                                            {
                                                try
                                                {
                                                    sum += z(i);
                                                }
                                                catch (Exception ex)
                                                {
                                                    Console.WriteLine(ex.Message);
                                                }
                                            }
                                        }
                                
                                        static long z(int i)
                                        {
                                            if (i == -100) throw new InvalidOperationException();
                                            return i % 2 == 0 ? i * (i - 1) : i * (1 - i);
                                        }
                                    }
                                


                                Для Release получаю:
                                Test 'Clean test #0'. Elapsed time: 928ms
                                Test 'TryCatch   #0'. Elapsed time: 901ms
                                Test 'Clean test #1'. Elapsed time: 897ms
                                Test 'TryCatch   #1'. Elapsed time: 899ms
                                Test 'Clean test #2'. Elapsed time: 888ms
                                Test 'TryCatch   #2'. Elapsed time: 888ms
                                Test 'Clean test #3'. Elapsed time: 890ms
                                Test 'TryCatch   #3'. Elapsed time: 888ms
                                Test 'Clean test #4'. Elapsed time: 892ms
                                Test 'TryCatch   #4'. Elapsed time: 889ms
                                Test 'Clean test #5'. Elapsed time: 888ms
                                Test 'TryCatch   #5'. Elapsed time: 892ms
                                Test 'Clean test #6'. Elapsed time: 889ms
                                Test 'TryCatch   #6'. Elapsed time: 893ms
                                Test 'Clean test #7'. Elapsed time: 887ms
                                Test 'TryCatch   #7'. Elapsed time: 892ms
                                Test 'Clean test #8'. Elapsed time: 889ms
                                Test 'TryCatch   #8'. Elapsed time: 884ms
                                Test 'Clean test #9'. Elapsed time: 938ms
                                Test 'TryCatch   #9'. Elapsed time: 915ms
                                


                                Для Debug (без оптимизации):
                                Test 'Clean test #0'. Elapsed time: 2247ms
                                Test 'TryCatch   #0'. Elapsed time: 2219ms
                                Test 'Clean test #1'. Elapsed time: 2187ms
                                Test 'TryCatch   #1'. Elapsed time: 2194ms
                                Test 'Clean test #2'. Elapsed time: 2204ms
                                Test 'TryCatch   #2'. Elapsed time: 2187ms
                                Test 'Clean test #3'. Elapsed time: 2200ms
                                Test 'TryCatch   #3'. Elapsed time: 2183ms
                                Test 'Clean test #4'. Elapsed time: 2178ms
                                Test 'TryCatch   #4'. Elapsed time: 2176ms
                                Test 'Clean test #5'. Elapsed time: 2180ms
                                Test 'TryCatch   #5'. Elapsed time: 2180ms
                                Test 'Clean test #6'. Elapsed time: 2224ms
                                Test 'TryCatch   #6'. Elapsed time: 2200ms
                                Test 'Clean test #7'. Elapsed time: 2175ms
                                Test 'TryCatch   #7'. Elapsed time: 2178ms
                                Test 'Clean test #8'. Elapsed time: 2174ms
                                Test 'TryCatch   #8'. Elapsed time: 2176ms
                                Test 'Clean test #9'. Elapsed time: 2174ms
                                Test 'TryCatch   #9'. Elapsed time: 2175ms
                                


                                По результатам, повода отказываться от использования этого подхода в шарпе не вижу.
                                • 0
                                  Теперь прогоните на Mono (желательно под Linux/OS X) и удивитесь.
                                  • +1
                                    К сожалению нет возможности, работаю только под win. Но если кто-нибудь запустит и покажет, с удовольствием удивлюсь.
                                    • +2
                                      Release
                                      Test 'Clean test #0'. Elapsed time: 285ms
                                      Test 'TryCatch   #0'. Elapsed time: 383ms
                                      
                                      Test 'Clean test #1'. Elapsed time: 285ms
                                      Test 'TryCatch   #1'. Elapsed time: 383ms
                                      
                                      Test 'Clean test #2'. Elapsed time: 286ms
                                      Test 'TryCatch   #2'. Elapsed time: 383ms
                                      
                                      Test 'Clean test #3'. Elapsed time: 284ms
                                      Test 'TryCatch   #3'. Elapsed time: 383ms
                                      
                                      Test 'Clean test #4'. Elapsed time: 294ms
                                      Test 'TryCatch   #4'. Elapsed time: 380ms
                                      
                                      Test 'Clean test #5'. Elapsed time: 292ms
                                      Test 'TryCatch   #5'. Elapsed time: 382ms
                                      
                                      Test 'Clean test #6'. Elapsed time: 287ms
                                      Test 'TryCatch   #6'. Elapsed time: 399ms
                                      
                                      Test 'Clean test #7'. Elapsed time: 286ms
                                      Test 'TryCatch   #7'. Elapsed time: 385ms
                                      
                                      Test 'Clean test #8'. Elapsed time: 287ms
                                      Test 'TryCatch   #8'. Elapsed time: 383ms
                                      
                                      Test 'Clean test #9'. Elapsed time: 287ms
                                      Test 'TryCatch   #9'. Elapsed time: 387ms
                                      


                                      Debug
                                      Test 'Clean test #0'. Elapsed time: 285ms
                                      Test 'TryCatch   #0'. Elapsed time: 385ms
                                      
                                      Test 'Clean test #1'. Elapsed time: 288ms
                                      Test 'TryCatch   #1'. Elapsed time: 395ms
                                      
                                      Test 'Clean test #2'. Elapsed time: 288ms
                                      Test 'TryCatch   #2'. Elapsed time: 387ms
                                      
                                      Test 'Clean test #3'. Elapsed time: 288ms
                                      Test 'TryCatch   #3'. Elapsed time: 387ms
                                      
                                      Test 'Clean test #4'. Elapsed time: 286ms
                                      Test 'TryCatch   #4'. Elapsed time: 386ms
                                      
                                      Test 'Clean test #5'. Elapsed time: 288ms
                                      Test 'TryCatch   #5'. Elapsed time: 394ms
                                      
                                      Test 'Clean test #6'. Elapsed time: 286ms
                                      Test 'TryCatch   #6'. Elapsed time: 386ms
                                      
                                      Test 'Clean test #7'. Elapsed time: 286ms
                                      Test 'TryCatch   #7'. Elapsed time: 387ms
                                      
                                      Test 'Clean test #8'. Elapsed time: 285ms
                                      Test 'TryCatch   #8'. Elapsed time: 385ms
                                      
                                      Test 'Clean test #9'. Elapsed time: 286ms
                                      Test 'TryCatch   #9'. Elapsed time: 388ms
                                      

                                  • +1
                                    Я, наверное, чего-то не понимаю, но в вашем коде же во время exception не будет брошен ни разу?
                                    • 0
                                      Именно так, добавил на всякий случай для компилятора
                                      • +1
                                        Тогда неудивительно, что вы не видите разницы: exception несет (по крайней мере, в нормативной реализации в CLR) ощутимые накладные расходы именно при бросании/обработке. У меня есть реальный пример в опыте, когда одно криво написанное место с эксепшном тормозило обработку пятимегабайтного пакета данных в тридцать раз.
                                        • –1
                                          Но тем не менее, в стеке вызовов все равно создаются фреймы для реализации блока try catch. По сути я смотрел накладные расходы именно на оборачивание, а не на обработку исключений, т.к. выброс исключения ситуация исключительная, программа в продакшене должна работать без них, а если они случаются, то затрачивание лишних, пусть даже секунд, на их обработку, наименьшая проблема.
                                          • 0
                                            выброс исключения ситуация исключительная, программа в продакшене должна работать без них

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

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

                                      P.S. В C# разве нету встроенного класса для бенчмарка?
                                      • 0
                                        Есть, но слишком простой тестик чтобы захотелось ими пользоваться. Stopwatch'у вроде можно доверять в таких замерах
                                        Класс Stopwatch основан на HPET (High Precision Event Timer, таймер событий высокой точности). Данный таймер был введён фирмой Microsoft, чтобы раз и навсегда поставить точку в проблемах измерения времени. Частота этого таймера (минимум 10 МГц) не меняется во время работы системы.


                                        Из этой статьи
                          • –10
                            А во всех случаях ответ один и тот же — потому что код написан неэффективно. Так зачем же предлагать заведомо неэффективное решение?

                            Вы же не притворяетесь, правда?
                            Код в статье был примером, служащим для демонстрации подхода.
                            Продолжайте вашу борьбу с мельницами дальше :)
                            • +2
                              Понятно, что в статье был пример того, что с этими ошибками можно сделать и как на них смотреть.
                              Но тем не менее, ответа на вопрос «что делать с кучей повторяющихся `if err != nil { return err }`» нет.
                              Ошибки в Go — это даже не ошибки, это просто «нафиг нам ошибки, пусть программеры обрабатывают значения, а еще мы сделаем возможность удобненько возвращать несколько значений из функции и всем объясним, что код ошибки должен быть вторым значением». Немного форсировали обработку ошибок тем, что код не скомпилируется, если не присвоить все возвращаемые функцией значения переменным или оставить неиспользуемые переменные.

                              Как по мне, с одной стороны го — это архитектура минимализма. С другой — всем было тупо лениво и и так сойдет, а программеры потом задолбаются писать один и тот же код тысячу раз и сделают вообще внешние утилиты-генераторы, а нам вообще впадлу думать и впихивать генерацию кода на уровне компилятора.
                              • –6
                                Но тем не менее, ответа на вопрос «что делать с кучей повторяющихся `if err != nil { return err }`.

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

                                что код ошибки должен быть вторым значением

                                Это не код ошибки, вы упускаете фундаментальную разницу.
                                • +8
                                  сделайте то же, чтобы вы делали с «кучей повторяющихся» кода
                                  Из поста в пост вы твердите одно и тоже. Но…

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

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

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

                                  Приведённый вами метод «обхода» проверок (пусть и в качестве примера), как справедливо замечает топик, не выдерживает вообще никакой критики. Мало того, что он безмерно уродлив (да-да), так он ещё и безмерно избыточен.
                                  • 0
                                    Вот вы справедливо заметили про «такая ситуация возникает очень редко». «Груда повторяющегося кода» — это признак плохого кода. Про циклическую сложность слышали? В Go есть даже линтер, которые показывает функции, в циклической сложностью больше 10. Если у вас 3 или 4 вызова, в котором вам нужно проверить ошибку — ничего тут страшного нет, зато любой, кто будет читать код после вас, сразу будет видеть, как идет flow и что происходит в результате ошибки.
                                  • +1
                                    это не важно: код ошибки, структура, строка или «ололо пиу-пиу упячка». Есть договоренность, как в питоне о том, что методы, наичнающиеся с двойного андерскора считаются приватными. В Го теперь договоренность., что вторым параметров возвращается ошибка. Да, я понимаю, что обрабатывать ошибку надо на общих основаниях, в общем-то. На общих основаниях с значениями. И как в каком-нибудь руби или яве нельзя(т.е. можно, но это совсем дискредитирует логику эксепшенов) 100 строк кода обернуть в один единый `rescue`, а надо делать ветвления(`rescue IOError ...`, `rescue ActiveRecordDamnExceptionPewPew...` etc), также и в Го нельзя взять и родить один простой «иф», а каждая ошибка будет решаться в индивидуальном порядке.
                                    Исключительно синтаксические примеры выглядят по дебильному:

                                    err := write(a);
                                    if err != nil { return err }
                                    err := write(b);
                                    if err != nil { return err }
                                    err := write;

                                    В реальной же жизни, как и в случае с эксепшенами, будет как-то так:

                                    user, err := find_user_by_id(user_id);
                                    if err == 33 { write(«User not found»); return }
                                    if err == 55 { write(«OMG can not connect to db, call admin! panic! aaa!»); notify_rollbar(...) }
                                    transaction_id, err := update_user_in_transaction(user, params)
                                    if err != nil { rollback_transaction(transaction_id); return }

                                    Безусловно, в реальной жизни тупых синтетических блоков `if err != nil { return }` нет. Точно также не было бы и «единого центра обработки эксепшенов» в какой-нибудь яве или руби т.к. каждый метод кинет свой эксепшен и по хорошему, надо их обработать индивидуально(херачить ветвление), а не заглушить всё самым базовым классом Exception.
                                    Однако это, епрст, не отменяет моего другого ощущения: «wtf???». В данном случае, это wtf исключительно про «договоренность» о том, что ошибку надо бы, по хорошему, возвращать вторым(последним) аргументом. Чтобы стандартно и красиво было, чтобы все как один, чтобы пользователю функции не приходилось гадать. И вот эти вот договорняки меня и расстраивают, в общем-то. Ну еще расстраивают и некоторые другие штуки в го, но они к данной теме не относятся.
                                    • –5
                                      Однако это, епрст, не отменяет моего другого ощущения: «wtf???»

                                      Ну это проходит, когда вы встречаетесь с кодом на эксепшенах, где ошибки вообще не обрабатываются как следует, просто потому что им не уделяют должное внимание, полагаясь на «механизмы языка». Это как раз причина, по которой исключения не используются в Google даже в С++.
                                      • +1
                                        Вот не надо гугл в качестве положительного примера приводить, от использования некоторых их либ, в частности, Skia, крайне негативный опыт. Зачастую в качестве возвращаемого значения приходит null вместо объекта без какой-либо детализации произошедшего. В итоге разбираться в причинах произошедшего приходится построчной трассировкой их кода с GDB наперевес.
                                        • +2
                                          И тут РНР обскакал Go :-P
                                          Fatal error: Uncaught exception 'Exception' with message
                                  • +2
                                    Код в статье был примером, служащим для демонстрации подхода.

                                    И этот пример как раз демонстрирует неэффективность.

                                    А как же получить одновременно эффективное (т.е., без избыточных вызовов) и при этом немногословное (т.е., без кучи if-ов) решение?
                                    • –4
                                      И этот пример как раз демонстрирует неэффективность.

                                      Этот пример демонстрирует подход к проблеме и ход мыслей. Остальное — надуманное вами лично.
                                      • +6
                                        Как получить одновременно эффективное (т.е., без избыточных вызовов) и при этом немногословное (т.е., без кучи if-ов) решение?
                                        • –7
                                          Вы меня, конечно, порядком, достали, но отвечу.

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

                                          Раз уж вы так хотите использовать в повторяющихся вызовах write() какие-то функции в качестве параметров, но не хотите, чтобы они вызывались в случае ошибки — сделайте так, чтобы они в этом случае не вызывались! Это же просто. Функции это такие же значения, и во write можно передавать саму функцию, а не ее результат, и запускать функцию после проверки на err. Кроме того, если вы уже настолько синтетически придумали пример, в котором лучше всего напрашивается выбрасывание «исключения» — используйте panic/recover! Go не запрещает это делать, он специально для этого в языке, чтобы использовать тогда, когда это действительно необходимо и оправданно.
                                          • +3
                                            Раз уж вы так хотите использовать в повторяющихся вызовах write() какие-то функции в качестве параметров, но не хотите, чтобы они вызывались в случае ошибки — сделайте так, чтобы они в этом случае не вызывались! Это же просто. Функции это такие же значения, и во write можно передавать саму функцию, а не ее результат, и запускать функцию после проверки на err.

                                            То есть сигнатура write меняется с «обычного» значения на функцию? Или добавляется новый метод с другим типом входного значения?

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

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

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

                                            Я ничего не придумывал, это пример из вашего поста.
                                            • –1
                                              Я ничего не придумывал, это пример из вашего поста.

                                              Это модифицированный пример из поста, который я переводил.

                                              • +3
                                                Его модификация не выходит за границы обычного ожидаемого сценария. Но самое главное, что поведение-то точно так же меняется и на исходном примере, просто там это менее очевидно.
                              • 0
                                Для данного условия можете воспользоваться panic() и recover() play.golang.org/p/vomyyTus6a
                                • +4
                                  А-га.

                                  Возьмем, значит, исходный код:

                                  _, err = fd.Write(p0[a:b])
                                  if err != nil {
                                      return err
                                  }
                                  _, err = fd.Write(p1[c:d])
                                  if err != nil {
                                      return err
                                  }
                                  _, err = fd.Write(p2[e:f])
                                  if err != nil {
                                      return err
                                  }
                                  


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

                                  Вот (ожидаемый) вывод:
                                  A
                                  Error: Error while writing B


                                  Окей, перепишем как предлагает Пайк (я добавил отладочный вывод внутрь errWriter.write, чтобы было видно происходящее). Результат:
                                  errWriter with A
                                  A
                                  errWriter with B
                                  errWriter with C
                                  Error: Error while writing B


                                  Тоже предсказуемо, для внутреннего объекта последовательность вызовов не изменилась, но снаружи их стало больше. Не ужас, но если снаружи есть тяжелые вычисления, может быть неприятно, поэтому, как вы предлагаете, воспользуемся panic/recover.

                                  Пишем «в лоб»:
                                  func (w *writer) write(s string) {
                                  	fmt.Println("errWriter with " + s)
                                  	_, err := w.Write(s)
                                  	if err != nil {
                                  		panic(err)
                                  	}
                                  }
                                  
                                  func victim() (err error) {
                                  	defer func() {
                                  		if r := recover(); r != nil {
                                  			errFromPanic, _ := r.(error)
                                  			err = errFromPanic
                                  
                                  		}
                                  	}()
                                  
                                  	var fd writer
                                  	fd.write("A")
                                  	fd.write("B")
                                  	fd.write("C")
                                  	return nil
                                  }
                                  


                                  Вывод ожидаем:
                                  errWriter with A
                                  A
                                  errWriter with B
                                  Error: Error while writing B


                                  Но, кажется, я ненастоящий гофер: сымитируем панику — и вывод становится неожиданным:
                                  Success


                                  Ну да, никогда не заглушайте ошибки. Следующая итерация работает почти как надо. Единственное отличие в том, что теперь паники выводятся как
                                  panic: Why? [recovered] panic: Why?
                                  и, кажется, потерей коллстека (а точно нет аналога throw;, может я просто искал плохо?).

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

                                  type errorWrapper struct {
                                  	err error
                                  }
                                  
                                  func (w *writer) write(s string) {
                                  	fmt.Println("errWriter with " + s)
                                  	_, err := w.Write(s)
                                  	if err != nil {
                                  		panic(errorWrapper{err: err})
                                  	}
                                  }
                                  
                                  func victim() (err error) {
                                  	defer func() {
                                  		if r := recover(); r != nil {
                                  			wrapper, ok := r.(errorWrapper)
                                  			if ok {
                                  				err = wrapper.err
                                  			} else {
                                  				panic(r)
                                  			}
                                  		}
                                  	}()
                                  
                                  	var fd writer
                                  	fd.write("A")
                                  	fd.write("B")
                                  	fd.write("C")
                                  	return nil
                                  }
                                  


                                  Ура! Работает (с теми же оговорками про изменение паники). Если кто видит edge case, который я по неопытности просмотрел — исправляйте.

                                  У меня только один вопрос: а чем это лучше try...catch?
                                  • 0
                                    У меня только один вопрос: а чем это лучше try...catch?
                                    А кто сказал, что эта дикая смесь ошибок и исключений лучше try/catch? Она не лучше, она намного, намного хуже.

                                    У меня только один вопрос: нафига писать такую дичь, полностью оторванную от реальных задач?
                                    • +1
                                      Посоветовали использовать panic для решения проблемы, описанной в посте. Я неправильно понял совет? Как надо было?
                                      • +6
                                        А покажите пожалуйста пример хорошей обработки ошибок в Go не оторванный от реальных задач.
                                      • 0
                                        Но, кажется, я ненастоящий гофер: сымитируем панику — и вывод становится неожиданным:
                                        Success

                                        Потому, что r.(error) вернёт nil. play.golang.org/p/zU6qWnI0kf
                                        • 0
                                          Да я знаю, почему. Легче не становится.
                                          • 0
                                            Ну, по крайней мере, дальнейшие варианты уже не имеют смысла совсем.
                                            • 0
                                              Какие «дальнейшие варианты» и почему не имеют смысла?
                                        • 0
                                          Кстати, прочитав статью blog.golang.org/errors-are-values, я так и не понял, чем не устроил вариант Пайка? play.golang.org/p/Y5AQy10QNU
                                          • 0
                                            Приблизительно вот этим:

                                            before A
                                            A
                                            before B
                                            before C
                                            before check
                                            OOps!
                                            


                                            Если в методе есть тяжелые вычисления, то это… обидно.
                                            • 0
                                              Если в методе есть тяжелые вычисления, то это… обидно.

                                              Тогда опять же, почему не вариант с panic/recover?
                                              • +1
                                                Ну вот тут рядом пишут, что это дичь какая-то. Мне он, простите за вкусовщину, тоже кажется уродливым.
                                                • 0
                                                  Как мне показалось, это относилось не к моему примеру.
                                                  • 0
                                                    Ваш пример не решает задачу, поставленную в посте — поведение функции меняется.
                                              • +1
                                                Вычисления — это еще что. А если вызываемый метод неявно меняет какой-то стейт?..
                                                • –1
                                                  Это уже, будем честными, и без обработки ошибок не самая лучшая практика.
                                                  • +1
                                                    Да, само собой. Но тем не менее, гарантий-то никаких нет.
                                      • –1
                                        Заминусовали бедного.
                                      • +2
                                        Астрологи объявили неделю ошибко в Го на диване. +4 поста.
                                        • +12
                                          «Errors are values» в Go завезли, удобного средства композиции всего этого — нет. Для примера удобного средства см. Rust с его Result или Haskell с его Either. Обидно, что особенности Go создают у людей предвзятую картину относительно такого подхода к обработке ошибок.
                                          • +4
                                            Чего всем так не нравятся «Errors are values»? Неужели нравится бесконечные лесенки из трай-кетчей городить?
                                            • +1
                                              Не нравится. Но цепочки if-ов нравятся не больше.
                                              • –11
                                                Мне не нравится механизм эксепшенов, но я не хожу во все посты про языки, где он используется и не рассказываю, как он мне не нравится.
                                                • +6
                                                  Если вы не заметили, мы сейчас в посте про обработку ошибок в Go. Не вижу ничего странного в том, чтобы обсуждать в нем… обработку ошибок в Go.
                                                  • –9
                                                    «Обсуждать» и рассказывать месяц за месяцем, как вам не нравится то, чем вы не пользуетесь — это разные вещи.
                                                    Обсуждение ценно, когда собеседник знает предмет обсуждения.
                                                    • +1
                                                      Да вроде бы в посте все написано про возможности Go в этом вопросе. Что еще нужно знать про ошибки в Go (которых там нет, а есть значения, я помню), чтобы их обсуждать?
                                                  • –1
                                                    Конечно не рассказываешь, потому что тебя заклюют. Эксепшоны — это известная common practice, Go тут в оппозиции.
                                              • +6
                                                Я вот не могу понять одну вещь… в статьях критикующих стандартный способ обработки ошибок в Go почему-то подразумевается, что всем умным людям опытным разработчикам понятно, что этот код ужасен плох. Но при этом я пока нигде не видел аргументированного объяснения, что же конкретно в нём плохо. Можете пояснить этот момент?

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

                                                Да, там дикий копипаст. Но проблема копипаста не в copy или paste, а в том, что дублируется логика, копируются баги которые потом нужно исправлять в куче почти идентичных мест, etc. — но в данном случае, конкретно с копированием этих трёх строчек — таких проблем не возникает. У нас вроде не секта, чтобы безусловно кричать «копипаст не пройдёт» или «goto это опиум для народа» — существуют вполне конкретные причины почему использование копипаста или goto вызывает проблемы, и существуют ситуации в которых эти причины не актуальны или менее важны чем польза от копипаста или goto.

                                                Да, это типовой код, практически 100% шаблон. И его лениво каждый раз набирать. Ну так а кто заставляет его набирать? Для таких вещей в текстовых редакторах существует поддержка snippet-ов. В vim, например, достаточно нажать 5 кнопок errn<Tab> чтобы вставить в код этот if на 3 строчки.

                                                Итого лично у меня получается следующее: код легко читать, быстро набирать, проблем с ним не возникает (в отличие от вышеупомянутых случаев, когда этот код пытаются «соптимизировать» засунув во вспомогательную функцию-хелпер, которая обладает целым букетом недостатков и крайне сомнительными достоинствами), единственная более-менее адекватная претензия — он не компактный. Но лично у меня 18 лет опыта работы с Perl, у которого с чем уж точно нет проблем, так это с компактностью кода… И на мой взгляд, хотя компактность кода иногда улучшает читабельность, гораздо чаще она её катастрофически ухудшает. А претензий к читабельности кода на Go лично у меня нет, так что возникает вопрос: а нужна ли на самом деле эта компактность кода? И для чего она нужна, если не для улучшения читабельности кода.
                                                • +3
                                                  В данном случае это не так — такой код читается очень легко, конструкции типовые, есть всего 2-4 варианта которые обычно используются, и которые со второго-третьего дня работы с Go начинаешь моментально распознавать.

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

                                                  конструкции типовые, есть всего 2-4 варианта которые обычно используются, и которые со второго-третьего дня работы с Go начинаешь моментально распознавать.

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

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

                                                  Нет, несомненно, «ошибки — это значения», и поэтому к ним применимы все языковые средства во всем их разнообразии… вот только униформность в этом случае сильно страдает, а вместе с ней начинают рассыпаться и приведенные вами аргументы.
                                                  • +2
                                                    Лично мне это читать тяжело.
                                                    Если это личная особенность, то это нормально. Все люди разные, и очень хорошо, что есть много разных языков программирования — каждый может подобрать себе такой, который ему понравится. Но личные предпочтения не могут являться аргументом при обсуждении языка — по определению кому-то что-то субъективно и немотивированно нравится, а что-то нет, и конструктивно здесь обсуждать просто нечего.
                                                    Я воспринимаю эти повторы как смысловой шум.
                                                    Это очень плохо — код, безусловно, выглядит намного проще и понятнее если из него выкинуть обработку ошибок, но дело в том, что обработка ошибок это важнейшая часть логики приложения, и даже бизнес-логики. Поэтому логика обработки ошибок должна быть максимально наглядной и легко доступной, т.е. находиться на виду, рядом с тем местом где ошибка возникла. Как и рекомендуется делать в Go.
                                                    Если я хочу понять бизнес, происходящий в коде, то я должен научиться отфильтровывать эти конструкции — но если я научусь это делать, вероятность того, что я пропущу в них что-то важное, резко увеличивается.
                                                    А вот здесь всё совсем наоборот. Фильтруются только типовые конструкции вроде if err != nil { return err } — за любое минимальное отличие в этой конструкции глаз сразу зацепляется, что сильно уменьшает вероятность пропустить что-то важное. Возможно, конечно, что здесь тоже имеют место отличия между нашими личными особенностями восприятия, но дело может быть и в том, что я на Go пишу, а Вы (как я понял из комментариев, прошу прощения если я ошибся) — нет, так что я опираюсь на практический опыт, а Вы на теоретическое впечатление сложившееся от чтения статей по Go. Раз уж Вас настолько интересует Go, что Вы постоянно критикуете его в комментариях, может быть Вам стоит потратить несколько недель и попробовать его в реальном проекте?
                                                    Впрочем, лично у меня к «стандартному способу обработки ошибок в Go» есть существенно более фундаментальная претензия, состоящая в том, что он… не стандартный. В стандартной библиотеке Go используются как минимум три разных способа сообщения об ошибочной ситуации
                                                    Стандартный способ обработки ошибок в Go — библиотеки не должны выкидывать наружу panic за исключением действительно редчайших особых ситуаций, а должны возвращать ошибку как значение. Покажите мне, пожалуйста, конкретные примеры кода стандартной библиотеки, о которых Вы говорите… хотя я подозреваю, что Вы просто подразумеваете что-то иное под «стандартным способом обработки ошибок в Go».
                                                    обсуждаемый выше пример с функцией-хелпером — это пример, приводимый в официальном блоге Go одним из авторов языка
                                                    Каждый может проявить слабость если сильно задолбать. Наверняка есть ситуации, когда такой хелпер вполне уместен — как и в случае с копипастом/goto у него есть и достоинства и недостатки, и существуют ситуации в которых достоинства перевешивают. Но я пока таких хелперов в реальном коде не встречал, так что эти ситуации скорее всего слишком редки, чтобы их имело смысл обсуждать в контексте глобальных особенностей языка.
                                                    Нет, несомненно, «ошибки — это значения», и поэтому к ним применимы все языковые средства во всем их разнообразии… вот только униформность в этом случае сильно страдает, а вместе с ней начинают рассыпаться и приведенные вами аргументы.
                                                    Извините, я, наверное, сильно устал, но я не уловил логики в этом утверждении. Можете развернуть мысль подробнее?
                                                    • +7
                                                      Если это личная особенность, то это нормально. Все люди разные, и очень хорошо, что есть много разных языков программирования — каждый может подобрать себе такой, который ему понравится. Но личные предпочтения не могут являться аргументом при обсуждении языка — по определению кому-то что-то субъективно и немотивированно нравится, а что-то нет, и конструктивно здесь обсуждать просто нечего.

                                                      Это говорит человек, который в начале треда написал «лично у меня получается следующее». Я, несомненно, с удовольствием посмотрю на объективную статистику по читаемости на больших выборках. Но пока мне греет душу одно: про приведенный в посте код сам Пайк пишет:

                                                      It is very repetitive. [...]


                                                      А вот после рефакторинга:

                                                      This is cleaner, even compared to the use of a closure, and also makes the actual sequence of writes being done easier to see on the page. There is no clutter any more. Programming with error values (and interfaces) has made the code nicer.


                                                      На мой взгляд, это описание совпадает с моим. То же самое относится к следующей вашей реплике:

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


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

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

                                                      Я уже говорил: у меня нет реального проекта, на котором я могу попробовать Go.

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

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

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

                                                      Покажите мне, пожалуйста, конкретные примеры кода стандартной библиотеки, о которых Вы говорите

                                                      (1)
                                                      func (b *Reader) ReadString(delim byte) (line string, err error)
                                                      
                                                      line, err := reader.ReadString('\n')
                                                      if err != nil {
                                                          // process the error
                                                      }
                                                      


                                                      (2)
                                                      func (s *Scanner) Scan() bool
                                                      
                                                      //***
                                                      for scanner.Scan() {
                                                          token := scanner.Text()
                                                          // process token
                                                      }
                                                      if err := scanner.Err(); err != nil {
                                                          // process the error
                                                      }
                                                      


                                                      (3)
                                                      func (z *Tokenizer) Next() TokenType
                                                      
                                                      //***
                                                      tt := z.Next()
                                                      if tt == html.ErrorToken {
                                                          // process the error
                                                      }
                                                      


                                                      Бонус-трек:
                                                      func (b *bufio.Writer) ReadFrom(r io.Reader) (n int64, err error)
                                                      
                                                      //***
                                                      b.Write(data)
                                                      if b.Flush() != nil {
                                                          return b.Flush()
                                                      }
                                                      
                                                      • 0
                                                        Извиняюсь, вот правильный код для бонуса (чтобы консистентно было):

                                                        func (b *bufio.Writer) Write(p []byte) (nn int, err error)
                                                        
                                                        //***
                                                        b.Write(data)
                                                        if err := b.Flush(); err != nil {
                                                            // process the error
                                                        }
                                                        
                                                • +4
                                                  Это все уже много раз обсуждалось. На самом деле, разработчики Go совсем чуточку обосрались. Оказывается, errors are values недостаточно. Вот какое шапито… те ошибки, которые Go не считает «исключительными», на самом деле, почти всегда cебе очень даже и исключительные. Не смогли записать в файл, не смогли открыть сокет с базой, не смогли то-се — это все исключительные ситуация, такого быть не должно. Валидация и тд — там все просто: да/нет, либо структура с ответом (error!). В принципе, ничего плохого в errors are values ошибках нет, учитывая тот факт, что они совершенно бесполезны (ошибки — это строки). В среднем случае, это все пишется в лог (без контекста и стектрейса, лол), а в лучшем — разраб вручную апкастит error до кастомного типа ошибки и кое-как вручную туда запихивает контекст и stacktrace.

                                                  Но я уверен, Пайку виднее.
                                                  • +4
                                                    те ошибки, которые Go не считает «исключительными», на самом деле, почти всегда cебе очень даже и исключительные
                                                    Исключительная ошибка или нет знает только конечное приложение. Библиотека этого знать не может в принципе. Поэтому использование panic в приложении вполне уместно, а в библиотеке крайне нежелательно. Кроме того, логика обработки ошибок — важнейшая часть логики приложения, поэтому крайне желательно побуждать программиста над ней задумываться, а не лепить механически panic везде просто потому, что ему лень думать.

                                                    Описываемый Вами стиль оформления ошибок в виде сложной структуры, как и любое абстрактно-универсальное решение, не является уместным везде и всегда. Там, где это имеет смысл — стандартная библиотека Go возвращает такие структуры (без контекста и стектрейса, там, где они нужны их несложно добавить: panic(err)). В абсолютном большинстве задач, с которыми работал я — текстового описания ошибки было абсолютно достаточно. и оно было намного удобнее сложной структуры. Желание всё усложнять без необходимости обычно проходит примерно через 10-15 лет работы программистом.

                                                    P.S. Кстати, поздравляю, Вы с этим шапито и немотивированными наездами на Пайка сформировали достаточно уникальный стиль речи, по которому автор текста определяется не хуже, чем по текстам Мицгола.
                                                    • –2
                                                      Ну смотри, ты не прав. Я считаю, что не стоит разделять на конечные пкг и приложения, не стоит этим заниматься. Go у нас очень открытый и черных ящиков нет — конечному приложению нужны стектрейсы, если что-то в библиотеке сломалось. Скажем так, я не вижу причины не использовать сквозные паники.
                                                      • +1
                                                        Ну смотри, ты не прав.
                                                        Великолепно! Кратко, и по сути!
                                                        Я считаю, что не стоит разделять на конечные пкг и приложения, не стоит этим заниматься.
                                                        Почему, собственно?
                                                        Различать разные сущности очень полезно — это позволяет каждую из них обрабатывать максимально специфичным для неё, т.е. более простым и эффективным, способом. Абстрактный и универсальный код — это в большинстве случаев либо тривиально и практически бесполезно, либо очень сложно и медленно.
                                                        конечному приложению нужны стектрейсы, если что-то в библиотеке сломалось.
                                                        Насколько сильно сломалось? Если очень сильно — и так будет паника. Если библиотека просто некорректно работает и вернула не то значение или ошибку — в её коде всё-равно придётся разбираться, отлаживать и фиксить, и поиск места где эту ошибку вернули (на которое бы указал стектрейс, и то только в случае возврата ошибки-исключения, а если библиотека просто возвращает некорректное значение то никакого стектрейса бы не было всё-равно) обычно занимает секунды/пару минут, которые полностью теряются на фоне времени необходимого на отладку и исправление кода (да и это время не является бесполезно потраченным — чтобы исправить ошибку всё-равно нужно разобраться почему она возникает, т.е. вычитать этот же самый код).
                                                        Скажем так, я не вижу причины не использовать сквозные паники.
                                                        Ну что тут скажешь… беда, просто беда. А я вот не вижу причины себя ими ограничивать, предпочитаю использовать тот подход, который лучше подходит в конкретной ситуации.
                                                        • –2
                                                          Я тебя понял, короче говоря.
                                                  • 0
                                                    А я думаю вот что.
                                                    Мне не нравятся исключения в существующем виде тем, что кроме вызова функции необходимо еще городить try-catch, причем если возвращаемое значение доступно из прототипа функции, то какие там функция выбросит исключения — тайна, для раскрытия которой нужно или читать документацию (если она есть и актуальна) или изучать код функции и всех функций, которые она вызывает. То есть — неявность.
                                                    С другой стороны, бесконечные if-else тоже не лучший вариант.
                                                    Откуда вывод — необходимо придумать что-то еще, совмещающее достоинства обоих методов и лишенное недостатков.

                                                    И вот какие мысли. Основной недостаток исключений — неявность. Поэтому сделаем их явными. Для начала, использование/неиспользование исключений в данном конкретном месте — это дело программиста. То есть если программист заключает функцию в try-catch с обработкой конкретного типа исключений — то этот тип исключений может генерироваться в данном коде. Иначе — не может, и должен генерироваться код возврата.
                                                    Хорошо бы в заголовке каждой функции обязать прописывать исключения, которые она в принципе может генерировать.
                                                    Принцип «ошибка это значение» тоже сохраняется. Для этого должен быть оператор, аналогичный return, но умеющий вместо возврата кода ошибки выбрасывать исключение, при его разрешенности в данном контексте вызова.
                                                    И наконец, переход от распространения исключения к возврату ошибки (хотя это самое сомнительное, но ладно — напишу): для этого, при отсутствии явных блоков try-catch, каждая функция является неявным блоком try-catch, превращающим любое исключение внутри себя в возврат кода ошибки по умолчанию для данной функции. Со всеми фичами исключений вроде раскрутки стека.

                                                    Выглядеть это должно так.
                                                    1. по умолчанию программист вызывает любую функцию, будучи уверенным, что она не выбросит исключений, а всегда вернет код возврата.
                                                    2. если программист хочет чтобы функция выбросила исключение и он готов его обработать, он просто разрешает раскрутку этого исключения для каждой функции, входящей в цепочку вызовов, до того места где исключение ловится; и второе — перед вызовом функции используется специальное ключевое слово (то же try), говорящее что мы готовы принять и исключение в том числе. Это наглядно, никаких неожиданностей — написано int x = try foo() значит мы не просто вызываем функцию, а «пытаемся вызвать, понимая что может и не получится». Городить catch при этом не нужно: если функция foo() вывалится с исключением, а у нас нет на нее catch — сработает catch по умолчанию, который конвертирует это в код возврата ошибки для данной функции. Исключение может распространяться дальше только в случае если мы разрешили это, указав в заголовке нашей функции что она может генерировать исключение такого типа.

                                                    Пока еще не во всем уверен до конца, но как-то так…
                                                      • +2
                                                        Откуда вывод — необходимо придумать что-то еще, совмещающее достоинства обоих методов и лишенное недостатков.

                                                        Мне очень неловко повторяться, но я, наверное, все-таки еще раз напомню про монаду Try. Она, конечно, недостатков не лишена, но вот уж явности ей более чем хватает (при этом if..else ей не обязательны).

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

                                                          Здравствуй, Java!
                                                          • 0
                                                            А в этом есть что-то плохое? (не писал никогда на Java, но на C++ реально напрягает когда не знаешь что там выкинет функция)
                                                            • 0
                                                              Как и всегда: что-то получаешь, что-то теряешь. Где-то на Хабре была статья как раз на тему того, почему это плохо: при большой вложенности вызовов приходится либо таскать эксепшенов (что громоздко), либо махнуть рукой и указать при объявлении метода, что он выкидывает обобщённый эксепшен (что сводит плюсы на нет).
                                                        • +5
                                                          Тут случай, когда макросы выручают. Спасибо Rust :)
                                                          fn write_smth(fd: &mut File) -> Result<()> {
                                                              try!(fd.write(b"blablabla"));
                                                              try!(fd.write(b"blabla"));
                                                              try!(fd.write(b"bla"));
                                                              Ok(())
                                                          }
                                                          
                                                          • +3
                                                            Можно еще через and_then определить:

                                                            fn write_smth(fd: &mut File) -> Result<()> {
                                                                fd.write(b"blablabla").and_then(|| fd.write(b"blabla")).and_then(|| fd.write(b"bla"))
                                                            }
                                                            
                                                            • 0
                                                              Привет, монада Maybe, записанная в явном виде.
                                                          • 0
                                                            Классическая проблема терминологии. Если продолжать называть значения «ошибками», то и восприниматься они будут как ошибки.
                                                            А возврат множественных значений — это не ошибки, в асинхронных языках это всего лишь один из вариантов результата выполнения функции.
                                                            $.ajax({
                                                               onOk200:()=>{},
                                                               onServerNotOkAnswer: ()=>{},
                                                               onNotConnect: ()=>{}
                                                               onCreated201: ()=>{}
                                                            });
                                                            

                                                            Просто с асинхронностью появляется проблема чтения кода, известная под названием «callback hell». А в go можно также линейно продолжать читать и писать код.
                                                            • 0
                                                              А с исключениями мы делаем предположение, что результаты разных функций будут совпадать (выдавать обобщенные исключения). Только при этом условии они нужны для возврата значения. А второй вариант использования — любое обобщение внутри одной функции, вроде пресловутого множественного elseif из статьи. Но обычно, это сигнал о нарушении SRP и необходимости создания новой функции, а не сигнал к созданию раздела done.

                                                              С пробросом вверх сразу возникает вопрос, какой степени обобщенности должно быть исключение, чтобы его смог разобрать кто-то выше чем непосредственно вызывающая функция. И в итоге приходим к тому, что либо мы обобщаем любые исключения до panic, warning, notice, deprecated, либо в обязательном порядке обрабатываем все варианты ответа функции непосредственно в месте вызова (как когда-то пытались сделать в Java с помощью throws), и как сделали в go более логичным образом.
                                                              No way.

                                                              P.S. Ах да, есть еще вариант обработки исключений специальным модулем, отдельно от основного потока. Но опять же это должны быть очень-очень обобщенные исключения, т.е. panic.
                                                            • –3
                                                              В Go был выбран метод panic/recover/defer для обработки ошибок вместо try/catch по той причине, что он более понятный. Try/catch часто неправильно используют даже опытные программисты, т.к. путают и перемешивают ошибки и исключения, не говорю уже о неопытных и начинающих программистах.
                                                              В каком случае невыполнение SQL-запроса — это ошибка, а в каком обычное исключение?
                                                              В Go такой путаницы нет.
                                                              • +7
                                                                Хм, а как Go рекомендует трактовать невыполнение SQL-запроса?
                                                                • –1
                                                                  Как один из вариантов результата выполнения функции exec, например.
                                                                  • 0
                                                                    И чем это отличается от (подставь свой язык)? В .net, скажем, ошибка при выполнении SQL-запроса — тоже «один из вариантов результата выполнения» соответствующего метода.
                                                                    • 0
                                                                      Отличается местом обработки результата, в go нельзя обработать результат выполнения функции непонятно где. В C# можно исключение ловить хоть в функции main, если речь об однопоточном приложении.
                                                                      • 0
                                                                        в go нельзя обработать результат выполнения функции непонятно где.

                                                                        Да, можно просто проигнорировать его.
                                                                        • 0
                                                                          Просто нельзя, только явно.
                                                                          • 0
                                                                            «Просто» тоже можно, если «другие результаты» функции не интересуют. Пример тут в посте есть уже.
                                                                            • 0
                                                                              Не вижу примера, где можно не получить все результаты функции при ее вызове. То, что можно после вызова проигнорировать, это уже на совести (ответственности) вызывающей стороны и не менее явно чем try{ a(); }catch(Exception e){}
                                                                              • 0
                                                                                Не вижу примера, где можно не получить все результаты функции при ее вызове.

                                                                                Я же говорю: если другие результаты вызова не интересуют. Например, если вы вызываете UPDATE в SQL. Или вот еще более милое:

                                                                                func (tx *Tx) Commit() error
                                                                                
                                                                                • 0
                                                                                  Под результатами функции я понимаю, именно множественные результаты одного вызова функции. Т.е. априори, в программировании практически не используются действительно чистые функции и под «функцией» подразумевается часть функциональности системы, а не отражение входного множества на выходное. Поэтому, в go не стали продолжать тянуть лямку бессмысленной затеи с одним результатом, как в других языках. И исчезла потребность в исключениях. Единственный аргумент за один результат функции был принцип SRP. Только понимали его не правильно из-за неправильного определения «функции». В математике задача функции — получить одно выходное значение на одно входное. В других областях задача функции — выполнить часть работы в своей области ответственности.
                                                                                  На самом деле, go — это только начало, следующая ступень, отказ от стека. Тогда мы вернемся к процедурам, по типу объектов EventEmitter, когда функция бросает исключения, но не как результат для вызывающей функции, а как бы во внешнюю среду. Тогда и с асинхронностью (читай, многопоточностью) проблем не будет. Вопрос только, как это адекватно описать, чтобы человеческий мозг не ломался при чтении.
                                                                                  • +1
                                                                                    Под результатами функции я понимаю, именно множественные результаты одного вызова функции.

                                                                                    Я тоже.

                                                                                    func (tx *Tx) Exec(query string, args ...interface{}) (Result, error)
                                                                                    


                                                                                    Если я выполняю UPDATE, и мне не важно, сколько строк обновлено, я могу просто проигнорировать весь возврат.

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

                                                                                    Эмм, а как же функциональное программирование?

                                                                                    fun funny i =
                                                                                     (i/2.0, i/3.0)
                                                                                    
                                                                                    let oneHalf, oneThird = funny 1
                                                                                    


                                                                                    Тогда и с асинхронностью (читай, многопоточностью) проблем не будет.

                                                                                    Программирование на continuations? Программирование на акторах? Имя им легион давно.
                                                                                    • 0
                                                                                      Если я выполняю UPDATE, и мне не важно, сколько строк обновлено, я могу просто проигнорировать весь возврат.

                                                                                      Покажите, как вы его проигноируете, вызовите, пожалуйста, функцию?
                                                                                      fun funny i =
                                                                                      (i/2.0, i/3.0)

                                                                                      Ну вот только для математических задач функциональное программирование и подходит, во всех других сферах функция обязательно имеет зависимости (внешние системы), а значит не может гарантировать один и тот же результат при одинаковых входных значениях. Отсюда return null, throw new Exception и т.д. Хотя действительным результатом выполнения функции будет куча вызов внешней системы.
                                                                                      Программирование на continuations? Программирование на акторах? Имя им легион давно.

                                                                                      Ну сейчас нет времени честно обсуждать каждый вариант, но я пока достойной реализации еще не видел, ИМХО.
                                                                                      • +2
                                                                                        Покажите, как вы его проигноируете, вызовите, пожалуйста, функцию?

                                                                                        tx.Exec("UPDATE Users SET Active = 0")
                                                                                        


                                                                                        во всех других сферах функция обязательно имеет зависимости (внешние системы)

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

                                                                                        Отсюда return null, throw new Exception и т.д. Хотя действительным результатом выполнения функции будет куча вызов внешней системы.

                                                                                        Знаете, в моей жизни было много функций, которые возвращали null или бросали исключения, при этом не имея никакой внешней зависимости. Даже у стандартных алгоритмов бывают недопустимые ситуации.
                                                                                        • –2
                                                                                          tx.Exec(«UPDATE Users SET Active = 0»)

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

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

                                                                                          Ну это и говорит о том, что даже без зависимостей, концепция возврата одного результата не работает. Нужны механизмы для описания функции с множественными результатами с разными типами (FileNotFoundException, «file content», null).
                                                                                          • +2
                                                                                            Ну для разработчика go этот код равносилен пустому catch.

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

                                                                                            Однако, сложность как раз вызвать все эти зависимости и собрать всевозможные результаты.

                                                                                            Никакой особой сложности, по большому счету. Собственно, это экстремальное применение DI.

                                                                                            Нужны механизмы для описания функции с множественными результатами с разными типами (FileNotFoundException, «file content», null).

                                                                                            Вы так говорите, как будто union type — это что-то совершенно уникальное и недостижимое. А то, что вы сейчас описываете — это типичный Try[Option[T]].
                                                                                            • –2
                                                                                              То есть разработчик Go наизусть помнит, какие функции возвращает ошибку, а какие — нет, и где такой вызов эквивалентен пустому catch, а где — нет?

                                                                                              Претензия не по существу. Смотреть документацию (или сигнатуру) функции перед ее использованием признак профессионализма, или трата времени, ведь если что, кто-то где-то бросит Exception, который кто-то где-то поймает? И разработчику go не нужны эквиваленты из других концепций.
                                                                                              Никакой особой сложности, по большому счету. Собственно, это экстремальное применение DI.

                                                                                              Особой сложности вообще нет, есть просто сложность, включающая число сочетаний результатов вызовов зависимостей (будь то Exception или return). И где-то нужно ее описать, хотите в DIC — ваше право, суть не меняется.
                                                                                              Вы так говорите, как будто union type — это что-то совершенно уникальное и недостижимое. А то, что вы сейчас описываете — это типичный Try[Option[T]].

                                                                                              Union type не интересен, повышает сложность и чтения и рефакторинга в разы, лучше уж сразу слабую типизацию.
                                                                                              Try — костыль, желание оставить один результат выполнения функции, зачем? Это обман разработчика мнимой чистотой функции.
                                                                                              • +2
                                                                                                Претензия не по существу. Смотреть документацию (или сигнатуру) функции перед ее использованием признак профессионализма,

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

                                                                                                Особой сложности вообще нет, есть просто сложность, включающая число сочетаний результатов вызовов зависимостей (будь то Exception или return).

                                                                                                Все та же система типов плюс их композиция (предпочтительно, конечно, монадическая)

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

                                                                                                Каким именно образом он повышает сложность? Можете на примере показать?

                                                                                                Try — костыль, желание оставить один результат выполнения функции, зачем?

                                                                                                Чтобы явно указать, что функция может возвращать значение с семантикой «ошибка».

                                                                                                Это обман разработчика мнимой чистотой функции.

                                                                                                Почему мнимой? Как вообще тип возвращаемого значения связан с чистотой?
                                                                                                • +1
                                                                                                  Код существенно чаще читается, чем пишется. Когда я читаю (бегло) код, я не хочу лезть смотреть сигнатуру каждой используемой функции. Я могу на глаз определить, проглочена ошибка, или нет?

                                                                                                  А вы можете на глаз определить, выкидывает функция Exception или возвращает null? Если да, склоняю голову. Каждый раз и не нужно лезть, нужно просто знать, либо лезть (чаще всего навести мышку), если память уже не позволяет.
                                                                                                  Каким именно образом он повышает сложность? Можете на примере показать?

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

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

                                                                                                  Ну чистая функция для одного входного значения всегда вернет то же самое выходное. А здесь она еще оказывается может вернуть ошибку, значит она не чистая, как бы мы не пытались костылями это прикрыть.
                                                                                                  • +3
                                                                                                    А вы можете на глаз определить, выкидывает функция Exception или возвращает null?

                                                                                                    В языках с исключениями полезно считать, что любая функция может их кинуть (это, в принципе, так). С null сложнее, но там можно приручить статический анализатор.

                                                                                                    Ну все просто, как я уже сказал, функция может иметь действительно разные результаты (например строку или объект ошибки), и union type будет таким, что пересечение будет минимальным (toString?)

                                                                                                    Извините, но union type — это объединение всех результатов.

                                                                                                    А зачем нам постоянно описывать этот тип, если можем просто указать, что может вернуться либо то, либо то?

                                                                                                    Ну так и делается: Either[string,int]

                                                                                                    Для меня это означает, что функция может возвращать 2 разных значения, а «семантика ошибка» — игра слов, скрывающая это.

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

                                                                                                    Ну чистая функция для одного входного значения всегда вернет то же самое выходное.

                                                                                                    Функция, возвращающая Try — тоже.

                                                                                                    Кстати, а мы тут точно не путаем чистые функции с детерминированными? Для дискуссии это не очень важно, но любопытно стало. Я считал, что чистая функция — это функция без побочных эффектов.

                                                                                                    А здесь она еще оказывается может вернуть ошибку

                                                                                                    Ошибка может быть реакцией на конкретный подвид входного значения. Например, у нас есть функция, которая считает кратчайший путь по направленному графу с весами. Есть алгоритм, который не умеет обрабатывать графы с отрицательными циклами. Соответственно, для такого алгоритма очень логично возвращать одно из трех: (а) кратчайший путь (б) никакого пути, если его в графе нет (ц) ошибку, если на пути встретился отрицательный цикл. Значение строго детерминировано входными данными, но при этом прекрасно покрывается семантикой Try[Option[Path]].
                                                                                                    • 0
                                                                                                      В языках с исключениями полезно считать, что любая функция может их кинуть (это, в принципе, так). С null сложнее, но там можно приручить статический анализатор.

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

                                                                                                      Извините, для использования, это пересечение всех результатов. Вопрос, зачем нам объединять не пересекающиеся результаты?

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

                                                                                                      Так объясните, зачем мне оборачивать в какие-то объединенные типы, если можно написать прямо, либо то, либо то? И ограничивать себя невозможностью трех вариантов? С какой целью?
                                                                                                      Кстати, а мы тут точно не путаем чистые функции с детерминированными? Для дискуссии это не очень важно, но любопытно стало. Я считал, что чистая функция — это функция без побочных эффектов.

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

                                                                                                      Ошибка может быть реакцией на конкретный подвид входного значения.

                                                                                                      Вы можете называть один из возможных результатов ошибкой, в go так и делают.
                                                                                                      Значение строго детерминировано входными данными, но при этом прекрасно покрывается семантикой Try[Option[Path]].

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

                                                                                                        Ну то есть на каждый вызов функции без проверки результата (включая пример из поста Пайка) надо реагировать как на code smell?

                                                                                                        Извините, для использования, это пересечение всех результатов. Вопрос, зачем нам объединять не пересекающиеся результаты?

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

                                                                                                        Так объясните, зачем мне оборачивать в какие-то объединенные типы, если можно написать прямо, либо то, либо то?

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

                                                                                                        Да, кстати, а в Go разве можно написать либо то, либо то? Мне кажется, что возврат функции в Go — это k переменных, отношения между которыми языком никак не проверяются. Я не прав?

                                                                                                        И ограничивать себя невозможностью трех вариантов?

                                                                                                        Нет никакого ограничения. disjoint union types — да, ниже правильно пишут, они же типы-суммы — могут собираться из произвольного количества типов. Более того, если надо, можно взять тип-произведение и получить ровно то же поведение, что в Go.

                                                                                                        Теперь представьте, что есть доп. условие, что алгоритм для графов размером более N, должен сохранить его в файл и не возвращать ничего.

                                                                                                        Ну и что? Декомпонуем алгоритм на две части, первая теперь возвращает вместо Try[Option[Path]]GraphResult(Path(Vertex..) | NotFound | NegativeCycle | GraphTooLarge), а вторая вызывает первую, и в случае последнего результата делает сохранение.

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

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

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

                                                                                                        Пример приведите, если не сложно. Я не очень понимаю, о чем вы говорите.
                                                                                                        • –2
                                                                                                          Ну то есть на каждый вызов функции без проверки результата (включая пример из поста Пайка) надо реагировать как на code smell?

                                                                                                          Я уже написал, что нужно знать, что делает функция, а не пытаться проанализировать ее по названию, а в целом, мое мнение, что да, именно так, именно об этом я и сказал, что для разработчика go — это будет равносильно пустому catch. А ответом на вопрос насколько это эффективно, является ответ, насколько много в реальных задачах функций, возвращающих четко один тип значения (т.е. без возможности ошибок). По моему опыту, это меньше 1% функций, да и то с оговоркой, что panic-ошибка все равно возможна (память кончилась у процесса).
                                                                                                          Потому что множества ошибок и полезных данных не пересекаются.

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

                                                                                                          Там где мне нужны монадические операции, я будут использовать обертывание (и даже без встроенной поддержки монад), но это не ответ на вопрос, зачем это везде? Мне далеко не всегда нужно передавать этот супер-пупер тип куда-то дальше, чем место вызова.
                                                                                                          Нет никакого ограничения. disjoint union types — да, ниже правильно пишут, они же типы-суммы — могут собираться из произвольного количества типов. Более того, если надо, можно взять тип-произведение и получить ровно то же поведение, что в Go.

                                                                                                          Так с этим я не спорю, я утверждаю, что необходим механизм. позволяющий вернуть несколько различных типов значений без оборачивания в новый тип, так как в большом количестве случаев, это будет бессмысленное действие.
                                                                                                          Ну и что? Декомпонуем алгоритм на две части, первая теперь возвращает вместо Try[Option[Path]] — GraphResult(Path(Vertex..) | NotFound | NegativeCycle | GraphTooLarge), а вторая вызывает первую, и в случае последнего результата делает сохранение.

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

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

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

                                                                                                            в целом, мое мнение, что да, именно так, именно об этом я и сказал, что для разработчика go — это будет равносильно пустому catch.

                                                                                                            Ну то есть вот такой код:

                                                                                                            write(p0[a:b])
                                                                                                            write(p1[c:d])
                                                                                                            write(p2[e:f])
                                                                                                            // простыня
                                                                                                            if err != nil {
                                                                                                                return err
                                                                                                            }
                                                                                                            


                                                                                                            Это code smell?

                                                                                                            Вопрос был, с какой целью нам это бесполезное действо?

                                                                                                            Оно не бесполезное. Мы сейчас к этому вернемся.

                                                                                                            зачем это везде? Мне далеко не всегда нужно передавать этот супер-пупер тип куда-то дальше, чем место вызова.

                                                                                                            А вот теперь вспомним вашу же фразу:

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


                                                                                                            Это означает, что 99% процентов функций (по вашим словам) возвращают либо ошибку, либо значение (набор значений). Это, в свою очередь, означает, что в 99% случаев вам нужна обработка ошибок, а ее, поверьте, в таких сценариях удобнее делать монадической композицией. Пример хотите, или на слово поверите?

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

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

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

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

                                                                                                            Ну во-первых, он нужен, чтобы определить формальный контракт функции. Во-вторых — ну окей, это называется анонимные типы-суммы. В OCaml есть (похожие) полиморфные варианты, в Rust собираются запилить вот прямо анонимы как есть.

                                                                                                            Возвращаем 3 варианта значений, либо путь, либо ошибку, либо запрос на запись в файл.

                                                                                                            Серьезно? Try[Either[Path | IO -> Try[unit]]]. Ну или Try[~[Path | None | IO -> Try[unit]]], если вспомнить, что пути может не быть, и взять анонимный тип.

                                                                                                            Наша композиция перестает зависеть от возможностей языка.

                                                                                                            Зависит-зависит.

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

                                                                                                              Ок, для вас информация скрыта, когда вы не читаете документацию (и сигнатуру), а пытаетесь угадать, что же делает этот функция по названию. Искренне извиняюсь, но для такого принципа управления сложностью у меня есть название «бабка ванга».
                                                                                                              Это code smell?

                                                                                                              Конечно, причем в любом языке.

                                                                                                              Это означает, что 99% процентов функций (по вашим словам) возвращают либо ошибку, либо значение (набор значений).

                                                                                                              Извините, но нет. Если A равно 1%, то это не значит, что B равно 99%. 99% в данном случае, будет 2 и более результатов, а не один результат и ошибка. 5 моих постов вам не хватило, чтобы понять, что кроме значения и ошибки могут быть еще результаты выполнения функции? Цитату привести, или на слово поверите?
                                                                                                              Не бессмысленное. Тип-произведение (в сочетании с матчингом) позволяет сделать все то же, что и прямой возврат нескольких значений, и кое-что еще, чего такой возврат не позволяет.

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

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

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

                                                                                                              Суть не меняется.
                                                                                                              Серьезно? Try[Either[Path | IO -> Try[unit]]]. Ну или Try[~[Path | None | IO -> Try[unit]]], если вспомнить, что пути может не быть, и взять анонимный тип.

                                                                                                              Либо лыжи не едут, либо что. Вы все пытаетесь соорудить новый тип там, где он просто не нужен.
                                                                                                              • +3
                                                                                                                Ок, для вас информация скрыта, когда вы не читаете документацию (и сигнатуру), а пытаетесь угадать, что же делает этот функция по названию. Искренне извиняюсь, но для такого принципа управления сложностью у меня есть название «бабка ванга».

                                                                                                                Странно, что его тогда рекомендует тот же МакКоннел.

                                                                                                                Конечно, причем в любом языке.

                                                                                                                Тогда зачем Пайк приводит его как валидный?

                                                                                                                Извините, но нет. Если A равно 1%, то это не значит, что B равно 99%. 99% в данном случае, будет 2 и более результатов, а не один результат и ошибка.

                                                                                                                Ну написано же: «либо ошибку, либо значение (набор значений)

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

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

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

                                                                                                                Эм, любая сигнатура функции с 2 результатами? (int, int)?

                                                                                                                Эта сигнатура как-то запрещает вернуть одновременно валидные значения в оба результата (скажем, (5, 3))?

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

                                                                                                                А как вы в языке с множественными результатами (только если можно, приведите конкретный пример на конкретном языке) опишете контракт функции вида «путь или путь не найден или найден отрицательный цикл» (все «или» — исключительные)?

                                                                                                                Суть не меняется.

                                                                                                                Меняется. Нового типа не возникает.

                                                                                                                Либо лыжи не едут, либо что. Вы все пытаетесь соорудить новый тип там, где он просто не нужен.

                                                                                                                Где вы видите новый тип?
                                                                                                                • –3
                                                                                                                  Почему? Как уже сказано, я ничего не теряю (потому что могу сделать все то же самое, что и с множественными результатами).

                                                                                                                  Лично я теряю время на бессмысленное усложнение.
                                                                                                                  Эта сигнатура как-то запрещает вернуть одновременно валидные значения в оба результата (скажем, (5, 3))?

                                                                                                                  Опять вы вводите понятие валидность, что говорит о том, что вы всем нутром хотите 1 результат. Нет, сигнатура никак не запрещает вернуть 2 результата, ведь для этого она и создана.
                                                                                                                  А как вы в языке с множественными результатами (только если можно, приведите конкретный пример на конкретном языке) опишете контракт функции вида «путь или путь не найден или найден отрицательный цикл» (все «или» — исключительные)?

                                                                                                                  Первые два результата будут отсутствовать. При вызове проверяем все.

                                                                                                                  Где вы видите новый тип?

                                                                                                                  Оборачивание в Try, Either, в юнион-типы, да во что угодно — бессмысленное синтаксическое усложнение кода. Как я уже написал — это новый тип возвращаемого значение, пусть будет безымянный тип, суть не меняется.
                                                                                                                  • +3
                                                                                                                    Лично я теряю время на бессмысленное усложнение.

                                                                                                                    Нет никакого усложнения.

                                                                                                                    fun goLike a =
                                                                                                                      (a*2, null)
                                                                                                                    
                                                                                                                    let (b, err) = goLike 3
                                                                                                                    


                                                                                                                    Опять вы вводите понятие валидность, что говорит о том, что вы всем нутром хотите 1 результат.

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

                                                                                                                    Нет, сигнатура никак не запрещает вернуть 2 результата, ведь для этого она и создана.

                                                                                                                    Значит, она не позволяет описать семантику «либо-либо».

                                                                                                                    Первые два результата будут отсутствовать.

                                                                                                                    Я же просил: конкретный пример. Вот прямо кодом, с конкретными типами.

                                                                                                                    Оборачивание в Try, Either, в юнион-типы, да во что угодно — бессмысленное синтаксическое усложнение кода.

                                                                                                                    Это если синтаксис таков, что вы не можете с этим комфортно работать. А в норме вы всего этого просто не видите, потому что автовывод.
                                                                                                                    • –2
                                                                                                                      Нет, это говорит о том, что у функции есть семантика. Большинство функций либо выдают (возвращают, бросают — не важно) ошибку, либо производят полезное действие (возвращают результат, не важно, одиночный или множественный, или произволят побочные эффекты). Соответственно, если мы получили от функции ошибку, нам не интересны возвращенные результаты (более того, в ряде случаев они опасны), поэтому если система типов позволяет явно сделать их недоступными — это уменьшает количество потенциальных ошибок.

                                                                                                                      1. У функции есть семантика, но она не говорит нам о том, что полезное действие должно быть одно. Это вы пытаетесь, за неимением инструментов, объединить результат этих полезных действий, зачем?
                                                                                                                      2. Приемочные тесты должен выполнять не тот, кто разрабатывает, ТЗ должен писать заказчик. Если с этим вы согласны, то почему функция должна решать, что является результатом, что ошибкой, что предупреждением, а что побочным эффектом? Семантика функции лишь определяет ее возможности, но никак не оценку результата.
                                                                                                                      «Удар рукой» определяет что нужно сделать, а не считать ошибкой промах. А если удара не было (сердце остановилось), то это не ошибка, а не исполнение функции, и к ней никакого отношения не имеет уж точно (в go, panic error). «Удар рукой по сопернику» предполагает как перемещение руки (в итоге, она окажется в другом месте), так и соприкосновение, мало того, соперник может уклониться и соприкосновения не будет, но функция отработала верно, при этом, что из этого «результат», что «побочный эффект», а что «ошибка» никак не может описать семантика «удар рукой по сопернику».
                                                                                                                      Я же просил: конкретный пример. Вот прямо кодом, с конкретными типами.

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

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

                                                                                                                        SRP?

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

                                                                                                                        Потому что это заложено в ее контракт.

                                                                                                                        Семантика функции лишь определяет ее возможности, но никак не оценку результата.

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

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

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

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

                                                                                                                        Какого обратного разделения? Какого разворачивания? Можете пояснить?
                                                                                                                        • –3
                                                                                                                          SRP?

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

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

                                                                                                                          Вот оцените ваш русский язык, «я гарантирую, но и не гарантирую, а еще когда я не гарантирую, то не гарантирую по-разному (вот вам список исключений), а если вам захочется узнать подробностей, вот тут у меня есть друг (Logger), которому я все сообщу тайком, но на самом деле вы еще как-то мне его контакты сообщите. Кстати, не забудьте, что бывает еще пара случаев, когда я вроде бы гарантирую, но с оговоркой, а это я сообщу еще одному гостю программы (WarningGuest), правда не забудьте его сами же и позвать».
                                                                                                                          Извините, но такие гарантии мне напоминают Почту России и русский авось. А я лучше уж заложу возможность потери посылки, кражи, плохой логистики и заранее предупрежу об этом клиента. А он учтет каждый из этих вариантов в своем бизнес-процессе. А не будет сидеть и ждать ошибки Почты. Некоторые называют это профессионализмом.
                                                                                                                          А что, просто так вы написать конкретный пример функции, выполняющей заданный контракт, вы не можете?

                                                                                                                          Я вам написал (int, int), вы его не поняли, значит, нужно сначала разобраться с теорией.
                                                                                                                          Какого обратного разделения? Какого разворачивания? Можете пояснить?

                                                                                                                          Есть два варианта использования различных упаковок (типа, монад). Первый вариант, тот который и задумывался — семантическая инкапсуляция, когда мы, ради читабельности на естесственном языке, подразумеваем под одним словом различные реальные действия, т.е. полиморфизм.
                                                                                                                          Второй вариант — из-за невозможности вернуть 2 результата одновременно (а желание возвращать один результат исходит из неправильного понимания слова «функция»), мы объединяем результаты не ради семантики, а потому что технически невозможно по другому. Чаще всего при этом рождается монстр-переменная или функция, состоящая из 2 слов Результат1ИлиРезультат2 или еще более непрозрачные термины, типа GraphResult. Единственная цель второго варианта в 99% случаев, тут же распаковать (выполнить) наш монадический контейнер и достать Результат1 и/или Результат2.
                                                                                                                          • +3
                                                                                                                            SRP и способ уведомления о результатах никак не связаны.

                                                                                                                            Зато SRP и количество «полезных действий» в функции связаны напрямую.

                                                                                                                            в языках с одним результатом — нет такой возможности, только «да или нет»

                                                                                                                            Это банальная неправда (я уж не знаю, от незнания или намеренно). Примеров выше по треду достаточно.

                                                                                                                            (про Go, в принципе, тоже можно поспорить, но не буду)

                                                                                                                            Вот оцените ваш русский язык,

                                                                                                                            Это не мой русский язык. С пугалами сражайтесь без меня.

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

                                                                                                                            Пожалуйста, приведите пример такой реализации.

                                                                                                                            Я вам написал (int, int)

                                                                                                                            Этот пример не имеет отношения к поставленной задаче. Напомню ее еще раз:

                                                                                                                            А как вы в языке с множественными результатами (только если можно, приведите конкретный пример на конкретном языке) опишете контракт функции вида «путь или путь не найден или найден отрицательный цикл» (все «или» — исключительные)?


                                                                                                                            Первый вариант, тот который и задумывался — семантическая инкапсуляция, когда мы, ради читабельности на естесственном языке, подразумеваем под одним словом различные реальные действия, т.е. полиморфизм.
                                                                                                                            Второй вариант — из-за невозможности вернуть 2 результата одновременно (а желание возвращать один результат исходит из неправильного понимания слова «функция»), мы объединяем результаты не ради семантики, а потому что технически невозможно по другому. Чаще всего при этом рождается монстр-переменная или функция, состоящая из 2 слов Результат1ИлиРезультат2 или еще более непрозрачные термины, типа GraphResult. Единственная цель второго варианта в 99% случаев, тут же распаковать (выполнить) наш монадический контейнер и достать Результат1 и/или Результат2.

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

                                                                                                                            А так — я искренне вас прошу перестать регулярно подменять типы-суммы (результат1 или результат2) типами-произведениями (результат1 и результат2). Возврат двух (или более) результатов из функции одновременно — это только второй сценарий. Первый вы при этом успешно игнорируете.
                                                                                                                            • +3
                                                                                                                              Да, по поводу «невозможности вернуть два результата одновременно».

                                                                                                                              Скажите, вот это — два результата одновременно?

                                                                                                                              fun divRem x y
                                                                                                                                //math
                                                                                                                                return (quotient, remainder)
                                                                                                                              
                                                                                                                              fun isOdd x
                                                                                                                                let (_, r) = divRem x 2
                                                                                                                                return r == 1
                                                                                                                              
                                                                                                                              let (q, r) = divRem 7 3
                                                                                                                              //q = 2
                                                                                                                              //r = 1
                                                                                                                              
                                                                                                                              let b = isOdd 9
                                                                                                                              //b = true
                                                                                                                              
                                                                                                                • 0
                                                                                                                  Справедливости ради стоит заметить, что «ошибки»и «результат» не всегда взаимоисключающи. Ошибки, которые могут сосуществовать с результатом называются обычно «предупреждениями». Exception и Option тут сосут.
                                                                                                                  • 0
                                                                                                                    (Вы уж определитесь, ошибки или предупреждения.)

                                                                                                                    Но за вычетом терминологии, это-то как раз тривиально:
                                                                                                                    let (res, warnings) = giveMeWarnings()

                                                                                                                    • 0
                                                                                                                      Опять же, «предупреждения» и «ошибки» тем более друг друга не исключают. И, кстати, ошибки тоже могут быть во множественном числе (и очень удобно, когда язык это таки поддерживает, а не вынуждает костылять исключение «список исключений»).

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

                                                                                                                      Лучшее решение выглядит так:

                                                                                                                      let res = getConfig()
                                                                                                                      when FileNotFound res = makeDefaultConfig()
                                                                                                                      when error is ValueIsEmpty return log( error ).ValueType.default
                                                                                                                      • 0
                                                                                                                        Опять же, «предупреждения» и «ошибки» тем более друг друга не исключают. [...] Опять же, что считать ошибкой, а что предупреждением, решать должен вызывающий код, а у вас получается это решает вызываемый.

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

                                                                                                                        А дальше уже вызывающий сам решит, падать ему, или откатываться.

                                                                                                                        let res = getConfig()
                                                                                                                        when FileNotFound res = makeDefaultConfig()
                                                                                                                        when error is ValueIsEmpty return log( error ).ValueType.default

                                                                                                                        Что-то мне это напоминает лисповские кондишны.
                                                                                                                        • 0
                                                                                                                          В том-то и дело, что вызываемый не обладает всей полнотой информации, чтобы принять решение ошибка это или предупреждение. Его задача — зафиксировать, что что-то пошло не так, как ожидалось, а задача вызывающего кода принять решения что делать (подставить дефолтное здачение, раскрутить стек, проигнорировать). И да, это лисповые кондишены и есть.
                                                                                                                          • –1
                                                                                                                            Я не уверен, что это решение хорошо для всех случаев — теперь вызывающий слишком много знает о внутренностях вызываемого. Но да, кондишны — штука очень мощная.
                                                                                                                            • 0
                                                                                                                              Не, вызывающий ничего не знает о внутренностях, он лишь предоставляет стратегии для разных кейсов. А уж как ими распорядиться решает вызываемый код.

                                                                                                                              if( val is null ) val = case ValueIsEmpty( val ) // тут null будет заменён на пустую строку
                                                                                                                              if( val.length > ValueType.maxLength ) val = case ValueTooLong( val ) // а тут сработает поведение по умолчанию — раскрутка стека.
                                                                                                                              if( val.length > ValueType.maxLength ) case WrongHandler( ValueTooLong ) // а тут в любом случае стек будет раскручен
                                                                                                                              return val
                                                                                                                              • –1
                                                                                                                                Для того, чтобы предоставлять стратегии, надо знать список кейсов. Иногда это оправдано, иногда — избыточно.
                                                                                                                                • 0
                                                                                                                                  Статического анализа вызываемых методов вполне достаточно, чтобы вывести пользователю список возможных кейсов. При этом не надо копипастить портянки возможных кейсов в сигнатуру каждой функции.
                                                                                                                                  • 0
                                                                                                                                    Статического анализа вызываемых методов вполне достаточно, чтобы вывести пользователю список возможных кейсов.

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

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

                                                                                                                                            Вот это и противоречит принципу сокрытия информации.
                                                                                                                                            • 0
                                                                                                                                              Нет такого принципа. Есть принцип инкапсуляция сложности — она к сокрытию информации никакого отношения не имеет.
                                                                                                                                              • 0
                                                                                                                                                «Information hiding is part of the foundation of both structured design and object-oriented design.»

                                                                                                                                                McConnell, Steve; Code Complete, 2nd ed; p. 92, «Hide Secrets (Information Hiding)»
                                                                                                                                                • 0
                                                                                                                                                  Ну, в качестве антипаттерна такой принцип есть, да :-)

                                                                                                                                                  Скрытие информации — один из способов реализации абстракций, который создаёт лишь дополнительные проблемы, когда требуются разные уровни абстракций. Яркие примеры, когда нужен низкий уровень абстракций — тесты, обобщённая сериализация. При этом для реализации абстракций вовсе не нужно скрывать информацию. Яркий пример — питон, где все поля объекта публичные и соответственно нет проблем с рефлексией. А яркий антипример — яваскрипт, в котором некоторые умники рьяно пытаются применить сокрытие информации, делая свои модули не просто нерасширяемыми, но и чертовски сложно отлаживаемыми.
                                                                                                                                                  • 0
                                                                                                                                                    Скрытие информации — один из способов реализации абстракций

                                                                                                                                                    Неа. Скрытие информации — это один из способов борьбы со сложностью, который, в частности, связан с абстракцией.

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

                                                                                                                                                    Например, какие?

                                                                                                                                                    Яркие примеры, когда нужен низкий уровень абстракций — тесты, обобщённая сериализация.

                                                                                                                                                    Тесты работают с реализацией, та