Судьба завела меня (программиста практика, в основном использующего 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)


  1. divan0
    18.11.2015 16:17
    -10

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


    1. lair
      18.11.2015 16:22
      +10

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

      Кстати, на этом примере как раз хорошо видна разница между го-шным «ошибки — это значения» и монадой Try, которая тоже трактует ошибки как значения.


      1. divan0
        18.11.2015 16:24
        -9

        +3 статьи и перевода по Go :)


      1. divan0
        18.11.2015 16:25
        -6

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

        Представьте, что речь не про ошибки, и ответьте сами на этот вопрос :)


        1. lair
          18.11.2015 16:28
          +7

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

          (кстати, это еще и не рефакторинг, вопреки тому, что написано в оригинальной статье)


          1. jj_killer
            18.11.2015 16:34
            +2

            Хотел уточнить, эффективно это ty-catch-finally для control flow?


            1. lair
              18.11.2015 16:56

              Зависит от реализации, очевидно. Может быть try-catch, может быть, композиция монад, может быть — тупая цепочка ифов.


              1. jj_killer
                18.11.2015 17:14
                +3

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

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

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

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


                1. divan0
                  18.11.2015 17:17

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

                  В Google исключения не используются нигде, в том числе в С++, именно по озвученной вами причине.


                  1. TrueMaker
                    19.11.2015 03:29

                    Ой не надо. Используются. Как минимум в 2010м использовались.


                1. lair
                  18.11.2015 17:20
                  +2

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

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

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

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

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

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

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


                  1. jj_killer
                    18.11.2015 17:35
                    +1

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

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


                    1. lair
                      18.11.2015 18:12
                      +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)
                        }
                      


                      (хотя, конечно, макросы выглядят красиво, не поспоришь)


                      1. jj_killer
                        18.11.2015 18:37
                        -3

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

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


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

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

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


                        1. lair
                          18.11.2015 18:43

                          Хорошо, а как тогда запретить использовать исключения во всех остальных случаях?

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

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

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

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

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

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

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

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

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


                          1. jj_killer
                            18.11.2015 18:54

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


                            1. lair
                              18.11.2015 19:08
                              +1

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


                              1. jj_killer
                                18.11.2015 19:11
                                +1

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


                      1. vintage
                        18.11.2015 18:49
                        -2

                        Так покрасивше всё ж:
                        fun fileDouble (path: string) {
                        return 2 * path.read.trim.parse!int
                        when FileNotFound return 0
                        }


                        1. lair
                          18.11.2015 18:50

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


                          1. vintage
                            18.11.2015 19:02
                            -2

                            В данном случае это Go + D + [немного фантазии].


                      1. kekekeks
                        18.11.2015 19:08
                        +1

                        исключения в .net ресурсоемки — но это не значит, что невозможно сделать нересурсоемкую реализацию исключений.
                        В Mono у них производительность примерно на порядок выше, если мне не изменяет память. Так что да, возможно, причём сохраняя совместимость с CLR.


                    1. Ogoun
                      18.11.2015 19:00
                      -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
                      


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


                      1. kekekeks
                        18.11.2015 19:10

                        Теперь прогоните на Mono (желательно под Linux/OS X) и удивитесь.


                        1. Ogoun
                          18.11.2015 19:15
                          +1

                          К сожалению нет возможности, работаю только под win. Но если кто-нибудь запустит и покажет, с удовольствием удивлюсь.


                          1. Prototik
                            19.11.2015 08:11
                            +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
                            


                      1. lair
                        18.11.2015 19:12
                        +1

                        Я, наверное, чего-то не понимаю, но в вашем коде же во время exception не будет брошен ни разу?


                        1. Ogoun
                          18.11.2015 19:13

                          Именно так, добавил на всякий случай для компилятора


                          1. lair
                            18.11.2015 19:25
                            +1

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


                            1. Ogoun
                              18.11.2015 19:34
                              -1

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


                              1. lair
                                18.11.2015 21:15

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

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

                                (заодно еще, конечно, синтаксис обработки нелинейный, но это свойство не только ресурсоемких исключений)


                      1. jj_killer
                        18.11.2015 19:15

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

                        P.S. В C# разве нету встроенного класса для бенчмарка?


                        1. Ogoun
                          18.11.2015 19:36

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

                          Класс Stopwatch основан на HPET (High Precision Event Timer, таймер событий высокой точности). Данный таймер был введён фирмой Microsoft, чтобы раз и навсегда поставить точку в проблемах измерения времени. Частота этого таймера (минимум 10 МГц) не меняется во время работы системы.


                          Из этой статьи


          1. divan0
            18.11.2015 16:36
            -10

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

            Вы же не притворяетесь, правда?
            Код в статье был примером, служащим для демонстрации подхода.
            Продолжайте вашу борьбу с мельницами дальше :)


            1. Cim
              18.11.2015 16:47
              +2

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

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


              1. divan0
                18.11.2015 16:56
                -6

                Но тем не менее, ответа на вопрос «что делать с кучей повторяющихся `if err != nil { return err }`.

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

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

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


                1. faiwer
                  18.11.2015 17:19
                  +8

                  сделайте то же, чтобы вы делали с «кучей повторяющихся» кода
                  Из поста в пост вы твердите одно и тоже. Но…

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

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

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

                  Приведённый вами метод «обхода» проверок (пусть и в качестве примера), как справедливо замечает топик, не выдерживает вообще никакой критики. Мало того, что он безмерно уродлив (да-да), так он ещё и безмерно избыточен.


                  1. divan0
                    18.11.2015 18:22

                    Вот вы справедливо заметили про «такая ситуация возникает очень редко». «Груда повторяющегося кода» — это признак плохого кода. Про циклическую сложность слышали? В Go есть даже линтер, которые показывает функции, в циклической сложностью больше 10. Если у вас 3 или 4 вызова, в котором вам нужно проверить ошибку — ничего тут страшного нет, зато любой, кто будет читать код после вас, сразу будет видеть, как идет flow и что происходит в результате ошибки.


                1. Cim
                  18.11.2015 18:06
                  +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 исключительно про «договоренность» о том, что ошибку надо бы, по хорошему, возвращать вторым(последним) аргументом. Чтобы стандартно и красиво было, чтобы все как один, чтобы пользователю функции не приходилось гадать. И вот эти вот договорняки меня и расстраивают, в общем-то. Ну еще расстраивают и некоторые другие штуки в го, но они к данной теме не относятся.


                  1. divan0
                    18.11.2015 18:25
                    -5

                    Однако это, епрст, не отменяет моего другого ощущения: «wtf???»

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


                    1. kekekeks
                      18.11.2015 19:16
                      +1

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


                    1. psylosss
                      18.11.2015 19:58
                      +2

                      И тут РНР обскакал Go :-P

                      Fatal error: Uncaught exception 'Exception' with message


            1. lair
              18.11.2015 16:58
              +2

              Код в статье был примером, служащим для демонстрации подхода.

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

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


              1. divan0
                18.11.2015 17:01
                -4

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

                Этот пример демонстрирует подход к проблеме и ход мыслей. Остальное — надуманное вами лично.


                1. lair
                  18.11.2015 17:01
                  +6

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


                  1. divan0
                    18.11.2015 17:16
                    -7

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

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

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


                    1. lair
                      18.11.2015 17:28
                      +3

                      Раз уж вы так хотите использовать в повторяющихся вызовах write() какие-то функции в качестве параметров, но не хотите, чтобы они вызывались в случае ошибки — сделайте так, чтобы они в этом случае не вызывались! Это же просто. Функции это такие же значения, и во write можно передавать саму функцию, а не ее результат, и запускать функцию после проверки на err.

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

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

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

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

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


                      1. divan0
                        18.11.2015 18:27
                        -1

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

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


                        1. lair
                          18.11.2015 18:29
                          +3

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


      1. Stronix
        18.11.2015 20:13

        Для данного условия можете воспользоваться panic() и recover() play.golang.org/p/vomyyTus6a


        1. lair
          18.11.2015 22:15
          +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?


          1. powerman
            18.11.2015 22:54

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

            У меня только один вопрос: нафига писать такую дичь, полностью оторванную от реальных задач?


            1. lair
              18.11.2015 23:25
              +1

              Посоветовали использовать panic для решения проблемы, описанной в посте. Я неправильно понял совет? Как надо было?


            1. poxu
              19.11.2015 10:42
              +6

              А покажите пожалуйста пример хорошей обработки ошибок в Go не оторванный от реальных задач.


          1. Stronix
            18.11.2015 23:03

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

            Потому, что r.(error) вернёт nil. play.golang.org/p/zU6qWnI0kf


            1. lair
              18.11.2015 23:24

              Да я знаю, почему. Легче не становится.


              1. Stronix
                19.11.2015 00:02

                Ну, по крайней мере, дальнейшие варианты уже не имеют смысла совсем.


                1. lair
                  19.11.2015 00:03

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


          1. Stronix
            18.11.2015 23:28

            Кстати, прочитав статью blog.golang.org/errors-are-values, я так и не понял, чем не устроил вариант Пайка? play.golang.org/p/Y5AQy10QNU


            1. lair
              18.11.2015 23:31

              Приблизительно вот этим:

              before A
              A
              before B
              before C
              before check
              OOps!
              


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


              1. Stronix
                18.11.2015 23:49

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

                Тогда опять же, почему не вариант с panic/recover?


                1. lair
                  19.11.2015 00:03
                  +1

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


                  1. Stronix
                    19.11.2015 00:08

                    Как мне показалось, это относилось не к моему примеру.


                    1. lair
                      19.11.2015 00:12

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


              1. nwalker
                19.11.2015 17:37
                +1

                Вычисления — это еще что. А если вызываемый метод неявно меняет какой-то стейт?..


                1. lair
                  19.11.2015 17:40
                  -1

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


                  1. nwalker
                    19.11.2015 17:45
                    +1

                    Да, само собой. Но тем не менее, гарантий-то никаких нет.


    1. catnikita255
      18.11.2015 23:06
      -1

      Заминусовали бедного.


  1. marapper
    18.11.2015 16:51
    +2

    Астрологи объявили неделю ошибко в Го на диване. +4 поста.


  1. corristo
    18.11.2015 16:52
    +12

    «Errors are values» в Go завезли, удобного средства композиции всего этого — нет. Для примера удобного средства см. Rust с его Result или Haskell с его Either. Обидно, что особенности Go создают у людей предвзятую картину относительно такого подхода к обработке ошибок.


  1. stepanp
    18.11.2015 18:07
    +4

    Чего всем так не нравятся «Errors are values»? Неужели нравится бесконечные лесенки из трай-кетчей городить?


    1. lair
      18.11.2015 18:14
      +1

      Не нравится. Но цепочки if-ов нравятся не больше.


      1. divan0
        18.11.2015 18:28
        -11

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


        1. lair
          18.11.2015 18:30
          +6

          Если вы не заметили, мы сейчас в посте про обработку ошибок в Go. Не вижу ничего странного в том, чтобы обсуждать в нем… обработку ошибок в Go.


          1. divan0
            18.11.2015 18:34
            -9

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


            1. lair
              18.11.2015 18:36
              +1

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


        1. namespace
          18.11.2015 18:59
          -1

          Конечно не рассказываешь, потому что тебя заклюют. Эксепшоны — это известная common practice, Go тут в оппозиции.


  1. powerman
    18.11.2015 18:43
    +6

    Я вот не могу понять одну вещь… в статьях критикующих стандартный способ обработки ошибок в Go почему-то подразумевается, что всем умным людям опытным разработчикам понятно, что этот код ужасен плох. Но при этом я пока нигде не видел аргументированного объяснения, что же конкретно в нём плохо. Можете пояснить этот момент?

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

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

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

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


    1. lair
      18.11.2015 19:07
      +3

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

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

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

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

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

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


      1. powerman
        18.11.2015 19:51
        +2

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


        1. lair
          18.11.2015 23:23
          +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()
          }
          


          1. lair
            19.11.2015 17:40

            Извиняюсь, вот правильный код для бонуса (чтобы консистентно было):

            func (b *bufio.Writer) Write(p []byte) (nn int, err error)
            
            //***
            b.Write(data)
            if err := b.Flush(); err != nil {
                // process the error
            }
            


  1. namespace
    18.11.2015 19:05
    +4

    Это все уже много раз обсуждалось. На самом деле, разработчики Go совсем чуточку обосрались. Оказывается, errors are values недостаточно. Вот какое шапито… те ошибки, которые Go не считает «исключительными», на самом деле, почти всегда cебе очень даже и исключительные. Не смогли записать в файл, не смогли открыть сокет с базой, не смогли то-се — это все исключительные ситуация, такого быть не должно. Валидация и тд — там все просто: да/нет, либо структура с ответом (error!). В принципе, ничего плохого в errors are values ошибках нет, учитывая тот факт, что они совершенно бесполезны (ошибки — это строки). В среднем случае, это все пишется в лог (без контекста и стектрейса, лол), а в лучшем — разраб вручную апкастит error до кастомного типа ошибки и кое-как вручную туда запихивает контекст и stacktrace.

    Но я уверен, Пайку виднее.


    1. powerman
      18.11.2015 20:12
      +4

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

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

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


      1. namespace
        18.11.2015 22:20
        -2

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


        1. powerman
          18.11.2015 22:50
          +1

          Ну смотри, ты не прав.
          Великолепно! Кратко, и по сути!
          Я считаю, что не стоит разделять на конечные пкг и приложения, не стоит этим заниматься.
          Почему, собственно?
          Различать разные сущности очень полезно — это позволяет каждую из них обрабатывать максимально специфичным для неё, т.е. более простым и эффективным, способом. Абстрактный и универсальный код — это в большинстве случаев либо тривиально и практически бесполезно, либо очень сложно и медленно.
          конечному приложению нужны стектрейсы, если что-то в библиотеке сломалось.
          Насколько сильно сломалось? Если очень сильно — и так будет паника. Если библиотека просто некорректно работает и вернула не то значение или ошибку — в её коде всё-равно придётся разбираться, отлаживать и фиксить, и поиск места где эту ошибку вернули (на которое бы указал стектрейс, и то только в случае возврата ошибки-исключения, а если библиотека просто возвращает некорректное значение то никакого стектрейса бы не было всё-равно) обычно занимает секунды/пару минут, которые полностью теряются на фоне времени необходимого на отладку и исправление кода (да и это время не является бесполезно потраченным — чтобы исправить ошибку всё-равно нужно разобраться почему она возникает, т.е. вычитать этот же самый код).
          Скажем так, я не вижу причины не использовать сквозные паники.
          Ну что тут скажешь… беда, просто беда. А я вот не вижу причины себя ими ограничивать, предпочитаю использовать тот подход, который лучше подходит в конкретной ситуации.


          1. namespace
            18.11.2015 23:43
            -2

            Я тебя понял, короче говоря.


  1. NeoCode
    18.11.2015 21:35

    А я думаю вот что.
    Мне не нравятся исключения в существующем виде тем, что кроме вызова функции необходимо еще городить try-catch, причем если возвращаемое значение доступно из прототипа функции, то какие там функция выбросит исключения — тайна, для раскрытия которой нужно или читать документацию (если она есть и актуальна) или изучать код функции и всех функций, которые она вызывает. То есть — неявность.
    С другой стороны, бесконечные if-else тоже не лучший вариант.
    Откуда вывод — необходимо придумать что-то еще, совмещающее достоинства обоих методов и лишенное недостатков.

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

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

    Пока еще не во всем уверен до конца, но как-то так…



    1. lair
      18.11.2015 22:18
      +2

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

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

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


    1. yar3333
      18.11.2015 22:47
      +4

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

      Здравствуй, Java!


      1. NeoCode
        18.11.2015 23:44

        А в этом есть что-то плохое? (не писал никогда на Java, но на C++ реально напрягает когда не знаешь что там выкинет функция)


        1. yar3333
          19.11.2015 02:40

          Как и всегда: что-то получаешь, что-то теряешь. Где-то на Хабре была статья как раз на тему того, почему это плохо: при большой вложенности вызовов приходится либо таскать эксепшенов (что громоздко), либо махнуть рукой и указать при объявлении метода, что он выкидывает обобщённый эксепшен (что сводит плюсы на нет).


  1. dbelka
    18.11.2015 22:10
    +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(())
    }
    


    1. defuz
      18.11.2015 23:08
      +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"))
      }
      


      1. nwalker
        19.11.2015 04:23

        Привет, монада Maybe, записанная в явном виде.


  1. arvitaly
    18.11.2015 23:17

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

    $.ajax({
       onOk200:()=>{},
       onServerNotOkAnswer: ()=>{},
       onNotConnect: ()=>{}
       onCreated201: ()=>{}
    });
    

    Просто с асинхронностью появляется проблема чтения кода, известная под названием «callback hell». А в go можно также линейно продолжать читать и писать код.


    1. arvitaly
      19.11.2015 01:49

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

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

      P.S. Ах да, есть еще вариант обработки исключений специальным модулем, отдельно от основного потока. Но опять же это должны быть очень-очень обобщенные исключения, т.е. panic.


  1. milast
    19.11.2015 06:40
    -3

    В Go был выбран метод panic/recover/defer для обработки ошибок вместо try/catch по той причине, что он более понятный. Try/catch часто неправильно используют даже опытные программисты, т.к. путают и перемешивают ошибки и исключения, не говорю уже о неопытных и начинающих программистах.
    В каком случае невыполнение SQL-запроса — это ошибка, а в каком обычное исключение?
    В Go такой путаницы нет.


    1. lair
      19.11.2015 09:42
      +7

      Хм, а как Go рекомендует трактовать невыполнение SQL-запроса?


      1. arvitaly
        19.11.2015 21:09
        -1

        Как один из вариантов результата выполнения функции exec, например.


        1. lair
          19.11.2015 21:33

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


          1. arvitaly
            19.11.2015 21:34

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


            1. lair
              19.11.2015 21:39

              в go нельзя обработать результат выполнения функции непонятно где.

              Да, можно просто проигнорировать его.


              1. arvitaly
                19.11.2015 21:40

                Просто нельзя, только явно.


                1. lair
                  19.11.2015 21:41

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


                  1. arvitaly
                    19.11.2015 21:46

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


                    1. lair
                      19.11.2015 21:48

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

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

                      func (tx *Tx) Commit() error
                      


                      1. arvitaly
                        19.11.2015 22:02

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


                        1. lair
                          19.11.2015 22:15
                          +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? Программирование на акторах? Имя им легион давно.


                          1. arvitaly
                            19.11.2015 22:22

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

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

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

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


                            1. lair
                              19.11.2015 22:38
                              +2

                              Покажите, как вы его проигноируете, вызовите, пожалуйста, функцию?

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


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

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

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

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


                              1. arvitaly
                                19.11.2015 22:53
                                -2

                                tx.Exec(«UPDATE Users SET Active = 0»)

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

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

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


                                1. lair
                                  19.11.2015 23:03
                                  +2

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

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

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

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

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

                                  Вы так говорите, как будто union type — это что-то совершенно уникальное и недостижимое. А то, что вы сейчас описываете — это типичный Try[Option[T]].


                                  1. arvitaly
                                    19.11.2015 23:13
                                    -2

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

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

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

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


                                    1. lair
                                      19.11.2015 23:28
                                      +2

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

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

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

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

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

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

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

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

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

                                      Почему мнимой? Как вообще тип возвращаемого значения связан с чистотой?


                                      1. arvitaly
                                        19.11.2015 23:48
                                        +1

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

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

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

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

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


                                        1. lair
                                          20.11.2015 00:24
                                          +3

                                          А вы можете на глаз определить, выкидывает функция Exception или возвращает null?

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

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

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

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

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

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

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

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

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

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

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

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


                                          1. arvitaly
                                            20.11.2015 01:07

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

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

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

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

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

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

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

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

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


                                            1. lair
                                              20.11.2015 01:17
                                              +2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                              Пример приведите, если не сложно. Я не очень понимаю, о чем вы говорите.


                                              1. arvitaly
                                                20.11.2015 01:41
                                                -2

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

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

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

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

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

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

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


                                                1. lair
                                                  20.11.2015 11:51
                                                  +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]]], если вспомнить, что пути может не быть, и взять анонимный тип.

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

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

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


                                                  1. arvitaly
                                                    20.11.2015 14:55
                                                    -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]]], если вспомнить, что пути может не быть, и взять анонимный тип.

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


                                                    1. lair
                                                      20.11.2015 15:03
                                                      +3

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


                                                      1. arvitaly
                                                        20.11.2015 15:24
                                                        -3

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

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

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

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

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

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


                                                        1. lair
                                                          20.11.2015 15:39
                                                          +3

                                                          Лично я теряю время на бессмысленное усложнение.

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

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


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

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

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

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

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

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

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

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


                                                          1. arvitaly
                                                            22.11.2015 01:59
                                                            -2

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

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

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

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


                                                            1. lair
                                                              22.11.2015 02:41
                                                              +2

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

                                                              SRP?

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

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

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

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

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

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

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

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


                                                              1. arvitaly
                                                                22.11.2015 03:40
                                                                -3

                                                                SRP?

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

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

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

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

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


                                                                1. lair
                                                                  22.11.2015 06:13
                                                                  +3

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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


                                                                1. lair
                                                                  22.11.2015 06:36
                                                                  +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
                                                                  


                                                  1. vintage
                                                    20.11.2015 15:44

                                                    Справедливости ради стоит заметить, что «ошибки»и «результат» не всегда взаимоисключающи. Ошибки, которые могут сосуществовать с результатом называются обычно «предупреждениями». Exception и Option тут сосут.


                                                    1. lair
                                                      20.11.2015 15:47

                                                      (Вы уж определитесь, ошибки или предупреждения.)

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


                                                      1. vintage
                                                        20.11.2015 16:05

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

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

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

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


                                                        1. lair
                                                          20.11.2015 16:22

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

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

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

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

                                                          Что-то мне это напоминает лисповские кондишны.


                                                          1. vintage
                                                            20.11.2015 16:38

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


                                                            1. lair
                                                              20.11.2015 16:40
                                                              -1

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


                                                              1. vintage
                                                                20.11.2015 17:10

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

                                                                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. lair
                                                                  20.11.2015 17:11
                                                                  -1

                                                                  Для того, чтобы предоставлять стратегии, надо знать список кейсов. Иногда это оправдано, иногда — избыточно.


                                                                  1. vintage
                                                                    20.11.2015 17:21

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


                                                                    1. lair
                                                                      20.11.2015 17:44

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

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

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


                                                                      1. vintage
                                                                        20.11.2015 17:54

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


                                                                        1. lair
                                                                          20.11.2015 17:56

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


                                                                          1. vintage
                                                                            20.11.2015 18:18

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


                                                                            1. lair
                                                                              20.11.2015 18:20
                                                                              -1

                                                                              Чем больше, тем лучше.

                                                                              Вот это и противоречит принципу сокрытия информации.


                                                                              1. vintage
                                                                                20.11.2015 18:38

                                                                                Нет такого принципа. Есть принцип инкапсуляция сложности — она к сокрытию информации никакого отношения не имеет.


                                                                                1. lair
                                                                                  20.11.2015 18:42

                                                                                  «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)»


                                                                                  1. vintage
                                                                                    20.11.2015 19:37

                                                                                    Ну, в качестве антипаттерна такой принцип есть, да :-)

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


                                                                                    1. lair
                                                                                      20.11.2015 20:51

                                                                                      Скрытие информации — один из способов реализации абстракций

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

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

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

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

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

                                                                                      Ну, в качестве антипаттерна такой принцип есть, да

                                                                                      Учитывая вышесказанное, я предпочту поверить в этом вопросе своему опыту (ну и заодно — МакКоннелу, Парнасу, Бруксу, исследованиям Боэма и Корсона-Ваишнави), нежели вам.


                                                                                      1. vintage
                                                                                        20.11.2015 21:30
                                                                                        -1

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

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

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

                                                                                        Ну вот, свели аргументированную дискуссию в плоскость веры :-D


                                                                                        1. lair
                                                                                          20.11.2015 21:36
                                                                                          +1

                                                                                          Сокрытие информации ни коим образом не поможет вам в борьбе со сложностью

                                                                                          Почему не поможет? То, чего я не знаю, не отнимает моего времени на обработку.

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

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

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

                                                                                          Зачем? Тестируйте по публичному контракту.

                                                                                          Типичный способ обойти сокрытие информации — запихивание тестовых сценариев напрямую в тестируемый класс в качестве методов.

                                                                                          Серьезно типичный? На мой взгляд, он отвратителен. Есть (как минимум в C#) много других замечательных способов это делать.

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

                                                                                          Ну и как этому мешает сокрытие информации?


                                                                                          1. vintage
                                                                                            20.11.2015 22:01

                                                                                            А как поможет? Доступность информации тоже не отнимает время на обработку.

                                                                                            Речь о программном доступе. Абстракции всегда текут.

                                                                                            Как протестировать инкапсулированный в объекте кэш исключительно через публичный интерфейс?
                                                                                            interface IURI { string getParam( string name ) }

                                                                                            Отвратителен, но работает. А что за замечательный способ есть в C#?

                                                                                            Как раскрытию информации мешает её сокрытие? Ну даже не знаю :-D


                                                                                            1. lair
                                                                                              20.11.2015 22:06
                                                                                              +4

                                                                                              Доступность информации тоже не отнимает время на обработку.

                                                                                              Отнимает. Любая избыточная информация требует ресурсов в голове.

                                                                                              Речь о программном доступе. Абстракции всегда текут.

                                                                                              А вот если вы полезли туда программно, значит, с дизайном все совсем плохо.

                                                                                              Как протестировать инкапсулированный в объекте кэш исключительно через публичный интерфейс?
                                                                                              interface IURI { string getParam( string name ) }

                                                                                              Слишком мало информации для ответа на вопрос. Но публичный интерфейс объекта, в любом случае, это не интерфейс в том смысле, который вы выдали.

                                                                                              Отвратителен, но работает. А что за замечательный способ есть в C#?

                                                                                              Самый простой — рефлексия. Еще есть наследование, internal и так далее.

                                                                                              Как раскрытию информации мешает её сокрытие?

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


                                                                                              1. vintage
                                                                                                20.11.2015 23:37
                                                                                                -1

                                                                                                Существование википедии наверно вообще мозг взрывает?

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

                                                                                                URI инициируется строкой, при запросе getParam он лениво парсится и полученная внутренняя структура помещается в кэш. Таким образом последующие вызовы getParam происходят гораздо быстрее. Как вы реализуете этот класс и как протестируете, что повторный вызов getParam минует парсинг?

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

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


                                                                                                1. lair
                                                                                                  20.11.2015 23:54
                                                                                                  +1

                                                                                                  Существование википедии наверно вообще мозг взрывает?

                                                                                                  Существование — нет. Необходимость ее знать — да.

                                                                                                  URI инициируется строкой, при запросе getParam он лениво парсится и полученная внутренняя структура помещается в кэш. Таким образом последующие вызовы getParam происходят гораздо быстрее. Как вы реализуете этот класс и как протестируете, что повторный вызов getParam минует парсинг?

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

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

                                                                                                  Эта «мания» может уменьшить расходы на модификацию программы в четыре раза.

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

                                                                                                  А кому не нужны — могут ими не пользоваться.

                                                                                                  Ну вот я и не хочу, чтобы они были в том интерфейсе, которым я пользуюсь.


                                                                                                  1. vintage
                                                                                                    21.11.2015 11:19
                                                                                                    -1

                                                                                                    Так вас никто и не принуждает всё знать.

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

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

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


                                                                                                    1. lair
                                                                                                      21.11.2015 12:39

                                                                                                      Так вас никто и не принуждает всё знать.

                                                                                                      Вот когда не принуждает — это и есть information hiding.

                                                                                                      Ну вот, вместо одного простого юнита мы имеем уже 3,

                                                                                                      Откуда три? Либо два вместо одного, либо три вместо двух.

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

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

                                                                                                      Сокрытие данных в этом случае приводит к неоправданному увеличению сложности

                                                                                                      Это не сокрытие данных, это дизайн под тесты. Расскажите, как бы вы это реализовывали, если бы сокрытие данных вам не «мешало»?

                                                                                                      эфемерное «может уменьшить расходы на модификацию программы в четыре раза»

                                                                                                      Оно не эфемерное. Korson, Timothy D., and Vijay K. Vaishnavi. 1986. “An Empirical Study of Modularity on Program Modifiability.”

                                                                                                      Каким образом наличие доступа к информации усложняет рефакторинг видимо так и останется без обоснования.

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

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

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

                                                                                                      А вот в том же JS, сокрытие фиг обойдёшь.

                                                                                                      Значит, в нем больше требований к дизайну.

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

                                                                                                      А как же милый сердцу любого программиста мир опен-соурса?

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


                                                                                                      1. vintage
                                                                                                        21.11.2015 13:49
                                                                                                        -1

                                                                                                        А когда вас не заставляют идти в школу — это тюремное заключение, ага.

                                                                                                        URI, URIParser, MockURIParser. Наверняка ещё и URICacher захочется вынести. Ах да, ещё и URISerializer. Сделаем звездолёт ради священного «хорошего дизайна»!

                                                                                                        То есть вы предлагаете ещё и URIFacade сверху прилепить, чтобы скрыть получившуюся «правильно задизайненную» сложность?

                                                                                                        Я просто проверю 3 кейса: parseURI формирует правильную структуру; getParam берёт данные из этой структуры; если структура не создана, то getParam её создаёт.

                                                                                                        Там сравнивается монолитный и модульный дизайн. При чём тут сокрытие информации?

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

                                                                                                        Ну да, всё, что не вписывается в догму сокрытия данных просто объявляется плохим дизайном :-) Главное — никогда не сомневаться в своих убеждениях.

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


                                                                                                        1. lair
                                                                                                          21.11.2015 14:33
                                                                                                          +3

                                                                                                          URI, URIParser, MockURIParser. Наверняка ещё и URICacher захочется вынести. Ах да, ещё и URISerializer. Сделаем звездолёт ради священного «хорошего дизайна»! То есть вы предлагаете ещё и URIFacade сверху прилепить, чтобы скрыть получившуюся «правильно задизайненную» сложность?

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

                                                                                                          Я просто проверю 3 кейса: parseURI формирует правильную структуру; getParam берёт данные из этой структуры; если структура не создана, то getParam её создаёт.

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

                                                                                                          Там сравнивается монолитный и модульный дизайн.

                                                                                                          Вы уже прочитали всю работу?

                                                                                                          При чём тут сокрытие информации?

                                                                                                          Вот при этом: www.ifsq.org/finding-dp-5.html

                                                                                                          Если оно используется, значит оно не избыточное.

                                                                                                          Далеко не факт. Ну и связность повышается, как уже сказано.

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

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

                                                                                                          Ну да, всё, что не вписывается в догму сокрытия данных просто объявляется плохим дизайном

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

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

                                                                                                          Эээ, а ничего. что «прятать сложность за простым интерфейсом» — это и есть (в числе прочего) information hiding?


                                                                                                          1. vintage
                                                                                                            21.11.2015 20:34

                                                                                                            1. Разделять IURIParser и IURISerializer — глупо, ибо они неразрывно связаны. Поэтому они должны быть частью одной абстракции — IURI.
                                                                                                            2. Формат структуры (привет, пародия на ооп в виде data-object) у вас внезапно становится частью публичного интерфейса.
                                                                                                            3. Парсинг опять же может происходить в несколько ленивых шагов — сначала парсим URI, потом парсим queryString. Прикрутим IQueryStringParser? В моём случае вся эта сложность инкапсулируется в одной абстракции — IURI.
                                                                                                            4. В вашем случае моки всё же придётся реализовать ввиду специфичного поведения.

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

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

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

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

                                                                                                            Пока что вы не привели ни одного аргумента за сокрытие информации, кроме:
                                                                                                            1. Как мне кажется это хороший дизайн
                                                                                                            2. Ссылки на сомнительное исследование


                                                                                                            1. lair
                                                                                                              21.11.2015 20:54
                                                                                                              +1

                                                                                                              1. Разделять IURIParser и IURISerializer — глупо, ибо они неразрывно связаны. Поэтому они должны быть частью одной абстракции — IURI.
                                                                                                              2. Формат структуры (привет, пародия на ооп в виде data-object) у вас внезапно становится частью публичного интерфейса.
                                                                                                              3. Парсинг опять же может происходить в несколько ленивых шагов — сначала парсим URI, потом парсим queryString. Прикрутим IQueryStringParser? В моём случае вся эта сложность инкапсулируется в одной абстракции — IURI.

                                                                                                              Все это решается кэширующим декоратором вокруг IURI.

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

                                                                                                              Какое же в них такой специфичное поведение, которое нельзя покрыть Moq?

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

                                                                                                              … а надо проверять то, что ожидает внешний код — отсутствие просадки по производительности.

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

                                                                                                              Да я, собственно, уже привел. Вам цитату целиком надо дать? (похоже, МакКоннела вы не читали) Да пожалуйста:

                                                                                                              Information hiding is one of the few theoretical techniques that has indisputably proven its value in practice, which has been true for a long time (Boehm 1987a). Large programs that use information hiding were found years ago to be easier to modify—by a factor of 4—than programs that don’t (Korson and Vaishnavi 1986).


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

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

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

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


                                        1. Leeb
                                          20.11.2015 00:36

                                          По-моему, lair под union type имел в виду не то, что вы подумали, а тип-сумму (в том смысле, в котором это понимает теория типов), или алгебраический тип данных. На Haskell это будет выглядеть примерно так

                                          data Maybe a = Just a | Nothing
                                          
                                          parseInt :: String -> Maybe Int
                                          parseInt s = ...
                                          


                                          Эта запись как раз и значит, что значение этого типа — это или одно (Just a), или другое (Nothing, ничего). Option[A] в Scala работает ровно так же. И никаких toString и прочей ерунды. Если нас интересует не просто отсутствие значения, а какая-то индикация в случае, когда значения нет (ошибка), существует тип Either, который можно параметризовать типами успешного и неуспешного результата (хотя в данном случае они абсолютно равнозначны, есть просто соглашение).

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

                                          Заметьте, функция не начинает возвращать несколько значений (если вам так хочется, вы, конечно, можете воспользоваться типом-произведением, иначе кортежем, но технически это будет всё равно одно значение). Не надо изобретать велосипед, эти вещи вполне себе известны давным давно и успешно применяются. Другое дело, что это всё тянет за собой параметрический полиморфизм, сопоставление с образцом, часто нормальную композицию функций, и вообще размышления о компонуемости примитивов в языке. Конечно, проще вернуть два значения (а если захочется 3? какое из них будет ошибкой?), и объявить всё остальное не нужным.

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


                                          1. arvitaly
                                            20.11.2015 01:49
                                            +1

                                            По-моему, lair под union type имел в виду не то, что вы подумали, а тип-сумму (в том смысле, в котором это понимает теория типов), или алгебраический тип данных.

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

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

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

                                            Конечно, проще вернуть два значения (а если захочется 3? какое из них будет ошибкой?

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

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

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


                                            1. Leeb
                                              20.11.2015 10:24
                                              +1

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

                                              Более того, АТД легко и просто позволяют кодировать 2 взаимоисключающих значения (именно поэтому они disjoint), то, что нужно, когда нам нужно вернуть значение или причину, по которой его вычисление не удалось. Вы же предлагаете всегда возвращать N значений, которые, чаще всего, взаимоисключающие, помечая отсутствие специальным значением. Согласитесь, что отсутствие значения (когда мы возвращаем одно значение, другого у нас просто нет, оно не null, не nil; оно отсутствует и взять его не откуда) и специальное значение, которое по договорённости означает отсутствие этого самого значения — вещи несколько различающиеся (не говоря уже о том, что второе вообще звучит, как бред).

                                              Я, кажется, каким-то отличным от вас образом понимаю значение слова «костыль».


                                              1. arvitaly
                                                20.11.2015 14:58
                                                -2

                                                Стоит начать с того, что АТД были созданы не только для возврата значений.

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

                                                Не естественным, а просто подогнали, как в Common Lisp якобы гладко легло ООП.
                                                Согласитесь, что отсутствие значения (когда мы возвращаем одно значение, другого у нас просто нет, оно не null, не nil; оно отсутствует и взять его не откуда) и специальное значение, которое по договорённости означает отсутствие этого самого значения — вещи несколько различающиеся (не говоря уже о том, что второе вообще звучит, как бред).

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


  1. maledog
    19.11.2015 15:42

    Выбросить пользователю исключение в 50 Mb или послать его по почте программисту?
    Сорри. не в ту веточку.