Как стать автором
Обновить

Комментарии 166

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Собственно говоря, мой вопрос о чем-то посредине остался без ответа :( Тут ниже уже написали по макрос для Rust, интересная тема, но тут опять же, надо были изначально закладывать такую возможность.
Под состоянием я имел ввиду логику исполнения (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)
  }


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

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


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

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

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

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

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

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

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

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

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

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

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

Монады были приведены к вашему упоминанию Rust, решение которого вам нравится — а монады, почему-то, нет.
К монадам у меня вполне индифферентное отношение. Но без доп. информации я не могу сказать почему этот вариант проигнорировали.
В смысле «почему его проигнорировали в Go»? Потому что адекватные монады нельзя сделать без дженериков, а дженериков в Go нет.
Понятно, что нельзя, я вот нарыл такой док, в свободное время прочитаю, потому что тема с дженериками постоянно всплывает, и опять же, верить на слово никому нельзя.
Так покрасивше всё ж:
fun fileDouble (path: string) {
return 2 * path.read.trim.parse!int
when FileNotFound return 0
}
Даже не буду спорить. Чем обеспечивается функциональность (я, к сожалению, на глаз язык не узнал).
В данном случае это Go + D + [немного фантазии].
исключения в .net ресурсоемки — но это не значит, что невозможно сделать нересурсоемкую реализацию исключений.
В Mono у них производительность примерно на порядок выше, если мне не изменяет память. Так что да, возможно, причём сохраняя совместимость с CLR.
Ради интереса сделал тест для шарпа:
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


По результатам, повода отказываться от использования этого подхода в шарпе не вижу.
Теперь прогоните на Mono (желательно под Linux/OS X) и удивитесь.
К сожалению нет возможности, работаю только под win. Но если кто-нибудь запустит и покажет, с удовольствием удивлюсь.
Release
Test 'Clean test #0'. Elapsed time: 285ms
Test 'TryCatch   #0'. Elapsed time: 383ms

Test 'Clean test #1'. Elapsed time: 285ms
Test 'TryCatch   #1'. Elapsed time: 383ms

Test 'Clean test #2'. Elapsed time: 286ms
Test 'TryCatch   #2'. Elapsed time: 383ms

Test 'Clean test #3'. Elapsed time: 284ms
Test 'TryCatch   #3'. Elapsed time: 383ms

Test 'Clean test #4'. Elapsed time: 294ms
Test 'TryCatch   #4'. Elapsed time: 380ms

Test 'Clean test #5'. Elapsed time: 292ms
Test 'TryCatch   #5'. Elapsed time: 382ms

Test 'Clean test #6'. Elapsed time: 287ms
Test 'TryCatch   #6'. Elapsed time: 399ms

Test 'Clean test #7'. Elapsed time: 286ms
Test 'TryCatch   #7'. Elapsed time: 385ms

Test 'Clean test #8'. Elapsed time: 287ms
Test 'TryCatch   #8'. Elapsed time: 383ms

Test 'Clean test #9'. Elapsed time: 287ms
Test 'TryCatch   #9'. Elapsed time: 387ms


Debug
Test 'Clean test #0'. Elapsed time: 285ms
Test 'TryCatch   #0'. Elapsed time: 385ms

Test 'Clean test #1'. Elapsed time: 288ms
Test 'TryCatch   #1'. Elapsed time: 395ms

Test 'Clean test #2'. Elapsed time: 288ms
Test 'TryCatch   #2'. Elapsed time: 387ms

Test 'Clean test #3'. Elapsed time: 288ms
Test 'TryCatch   #3'. Elapsed time: 387ms

Test 'Clean test #4'. Elapsed time: 286ms
Test 'TryCatch   #4'. Elapsed time: 386ms

Test 'Clean test #5'. Elapsed time: 288ms
Test 'TryCatch   #5'. Elapsed time: 394ms

Test 'Clean test #6'. Elapsed time: 286ms
Test 'TryCatch   #6'. Elapsed time: 386ms

Test 'Clean test #7'. Elapsed time: 286ms
Test 'TryCatch   #7'. Elapsed time: 387ms

Test 'Clean test #8'. Elapsed time: 285ms
Test 'TryCatch   #8'. Elapsed time: 385ms

Test 'Clean test #9'. Elapsed time: 286ms
Test 'TryCatch   #9'. Elapsed time: 388ms

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

У меня только один вопрос: нафига писать такую дичь, полностью оторванную от реальных задач?
Посоветовали использовать panic для решения проблемы, описанной в посте. Я неправильно понял совет? Как надо было?
А покажите пожалуйста пример хорошей обработки ошибок в Go не оторванный от реальных задач.
Да я знаю, почему. Легче не становится.
Ну, по крайней мере, дальнейшие варианты уже не имеют смысла совсем.
Какие «дальнейшие варианты» и почему не имеют смысла?
Приблизительно вот этим:

before A
A
before B
before C
before check
OOps!


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

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

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

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

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

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

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

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

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

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

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

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

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()
}
Извиняюсь, вот правильный код для бонуса (чтобы консистентно было):

func (b *bufio.Writer) Write(p []byte) (nn int, err error)

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

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

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

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

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

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

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

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

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

Здравствуй, Java!
А в этом есть что-то плохое? (не писал никогда на Java, но на C++ реально напрягает когда не знаешь что там выкинет функция)
Как и всегда: что-то получаешь, что-то теряешь. Где-то на Хабре была статья как раз на тему того, почему это плохо: при большой вложенности вызовов приходится либо таскать эксепшенов (что громоздко), либо махнуть рукой и указать при объявлении метода, что он выкидывает обобщённый эксепшен (что сводит плюсы на нет).
Тут случай, когда макросы выручают. Спасибо Rust :)
fn write_smth(fd: &mut File) -> Result<()> {
    try!(fd.write(b"blablabla"));
    try!(fd.write(b"blabla"));
    try!(fd.write(b"bla"));
    Ok(())
}
Можно еще через 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"))
}
Привет, монада Maybe, записанная в явном виде.
Классическая проблема терминологии. Если продолжать называть значения «ошибками», то и восприниматься они будут как ошибки.
А возврат множественных значений — это не ошибки, в асинхронных языках это всего лишь один из вариантов результата выполнения функции.
$.ajax({
   onOk200:()=>{},
   onServerNotOkAnswer: ()=>{},
   onNotConnect: ()=>{}
   onCreated201: ()=>{}
});

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

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

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

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

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

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

Я тоже.

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? Программирование на акторах? Имя им легион давно.
Если я выполняю UPDATE, и мне не важно, сколько строк обновлено, я могу просто проигнорировать весь возврат.

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Возвращаем 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]]], если вспомнить, что пути может не быть, и взять анонимный тип.

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

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

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

Ок, для вас информация скрыта, когда вы не читаете документацию (и сигнатуру), а пытаетесь угадать, что же делает этот функция по названию. Искренне извиняюсь, но для такого принципа управления сложностью у меня есть название «бабка ванга».
Это 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]]], если вспомнить, что пути может не быть, и взять анонимный тип.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

fun goLike a =
  (a*2, null)

let (b, err) = goLike 3


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

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

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

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

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

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

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

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

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

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

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

SRP?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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
Справедливости ради стоит заметить, что «ошибки»и «результат» не всегда взаимоисключающи. Ошибки, которые могут сосуществовать с результатом называются обычно «предупреждениями». Exception и Option тут сосут.
(Вы уж определитесь, ошибки или предупреждения.)

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

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

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

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

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

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

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

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

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

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
Для того, чтобы предоставлять стратегии, надо знать список кейсов. Иногда это оправдано, иногда — избыточно.
Статического анализа вызываемых методов вполне достаточно, чтобы вывести пользователю список возможных кейсов. При этом не надо копипастить портянки возможных кейсов в сигнатуру каждой функции.
Статического анализа вызываемых методов вполне достаточно, чтобы вывести пользователю список возможных кейсов.

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

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

Вот это и противоречит принципу сокрытия информации.
Нет такого принципа. Есть принцип инкапсуляция сложности — она к сокрытию информации никакого отношения не имеет.
«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)»
Ну, в качестве антипаттерна такой принцип есть, да :-)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Как раскрытию информации мешает её сокрытие? Ну даже не знаю :-D
Доступность информации тоже не отнимает время на обработку.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

Я нигде и не говорил, что это синонимы. Сокрытие информации — это один из инструментов управления сложностью.
По-моему, 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?
По-моему, lair под union type имел в виду не то, что вы подумали, а тип-сумму (в том смысле, в котором это понимает теория типов), или алгебраический тип данных.

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

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

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

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

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

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

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

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

Я, кажется, каким-то отличным от вас образом понимаю значение слова «костыль».
Стоит начать с того, что АТД были созданы не только для возврата значений.

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

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

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

Публикации