Судьба завела меня (программиста практика, в основном использующего C#) на проект, в котором основной функционал разрабатывается на Go.
Изучая Go обратил внимание на непривычную практику обработки ошибок. Почитав разъяснения в статьях Ошибки — это значения и в Почему «ошибки это значения» в Go отметил, что предлагаемые там решения заставляют вспомнить одну особенность Visual Basic, которую очень не лестно комментировали программисты.
Суть в следующем. Существуют жалобы программистов про проверку ошибок в Go. Возьмём пример из статьи Ошибки — это значения:
Мы видим, что присутствует повторяющий код, связанный с обработкой ошибок, который и вызывает нарекания. Вроде бы, почему бы не использовать стандартную практику try-catch и ошибка будет обрабатываться в одном месте?
Далее в статье Ошибки — это значения предлагается решение:
Здесь мы видим, что обработка ошибки теперь происходит один раз. Вроде бы, код упростился.
И тут, у меня возникло ощущение, что что-то подобное в истории было. И было это в Visual Basic и связано это было с оператором If. Буду говорить о Visual Basic в прошедшем времени, так как давно не работал с ним.
В Visual Basic присутствовал оператор If и в отличии от C++ и Java отсутствовал оператор ?:. Таким образом, записывался такой код:
Со стороны VB программистов были жалобы: много повторяющегося кода, хорошо бы его сократить до одной строки и иметь возможность использовать inline. Так, возможно, появился IIf:
Всё бы хорошо, но у народа начало нарастать другое возмущение, так как здесь оказался подвох. Дело в том, что помимо кода, когда в IIf используются значения, существует код, когда в место значений указываются функции и ожидается, что будет вызвана одна из функций в зависимости от условия, как в операторе If:
И для многих программистов было неожиданностью то, что в данном коде будут вызваны обе функции: GetTrueAValue и GetFalseAValue. Хотя они ожидали, что IIf будет работать, как оператор If (или как ?: в С++), т.е. они не понимали, какой смысл вызывать вторую функцию, когда её результирующее значение не имеет смысла.
Дело оказалось в том, что некоторые программисты по началу воспринимали IIf как оператор If, но на деле IIf оказался не оператором, а функцией. А чтобы вызвать функцию требуется вычислить все её аргумент, т.е. потребуется вызвать все указанные функции в аргументах. Причем, IIf был так удобно подпихнут, что не все программисты улавливали сразу, что это не оператор, а обычная функция.
Суть оператор If в Visual Basic (да и в других языках) определять участки кода, которые должны выполняться в зависимости от условия. Попытка использовать IIf дало лишь поверхностное решение и не оправдало основного ожидания от неё – вызывать ту или иную функцию в зависимости от условия.
Так вот. Вернёмся к Go и к практике, которую предлагают для работы с ошибками. У меня, сложилось такое впечатление, что выше приведённая практика обработки ошибок очень напоминает IIf в Visual Basic. Расширим пример:
Соответственно, здесь возникает вопрос: если в строке ew.write(getAB()) возникает ошибка, то почему выполняются getCD() и getEF(), когда в их результирующих значениях смысла нет?
Почему возникает такой вопрос? Дело в том, что ошибка — это значение, которое определяет, какой код должен выполняться. И это четко видно в первоначальном способе обработки ошибок в Go.
Значение ошибки обрабатывается оператором if. И обработка значения ошибки в if как раз и определяет, какой код должен быть выполнен дальше. При чём в большинстве случаев для принятия решения, важно наличие ошибки, а не её содержания.
По моему мнению, ty-catch-finally как раз и занимаются тем, чем нужно, определяют код, который будет выполняться в зависимости от возникшего исключения.
P.S.: Тут вспомнилось из VB:
Но лучше бы не вспоминалось. Чур меня, чур.
Изучая Go обратил внимание на непривычную практику обработки ошибок. Почитав разъяснения в статьях Ошибки — это значения и в Почему «ошибки это значения» в Go отметил, что предлагаемые там решения заставляют вспомнить одну особенность Visual Basic, которую очень не лестно комментировали программисты.
Суть в следующем. Существуют жалобы программистов про проверку ошибок в Go. Возьмём пример из статьи Ошибки — это значения:
_, err = fd.Write(p0[a:b])
if err != nil {
return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
return err
}
// and so on
Мы видим, что присутствует повторяющий код, связанный с обработкой ошибок, который и вызывает нарекания. Вроде бы, почему бы не использовать стандартную практику try-catch и ошибка будет обрабатываться в одном месте?
Далее в статье Ошибки — это значения предлагается решение:
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}
w := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
return ew.err
}
Здесь мы видим, что обработка ошибки теперь происходит один раз. Вроде бы, код упростился.
И тут, у меня возникло ощущение, что что-то подобное в истории было. И было это в Visual Basic и связано это было с оператором If. Буду говорить о Visual Basic в прошедшем времени, так как давно не работал с ним.
В Visual Basic присутствовал оператор If и в отличии от C++ и Java отсутствовал оператор ?:. Таким образом, записывался такой код:
Dim a As Integer
If CheckState() Then
a = 12
Else
a = 13
End If
Со стороны VB программистов были жалобы: много повторяющегося кода, хорошо бы его сократить до одной строки и иметь возможность использовать inline. Так, возможно, появился IIf:
a = IIf(CheckState(), 12, 13)
Всё бы хорошо, но у народа начало нарастать другое возмущение, так как здесь оказался подвох. Дело в том, что помимо кода, когда в IIf используются значения, существует код, когда в место значений указываются функции и ожидается, что будет вызвана одна из функций в зависимости от условия, как в операторе If:
a = IIf(CheckState(), GetTrueAValue(), GetFalseAValue())
И для многих программистов было неожиданностью то, что в данном коде будут вызваны обе функции: GetTrueAValue и GetFalseAValue. Хотя они ожидали, что IIf будет работать, как оператор If (или как ?: в С++), т.е. они не понимали, какой смысл вызывать вторую функцию, когда её результирующее значение не имеет смысла.
Дело оказалось в том, что некоторые программисты по началу воспринимали IIf как оператор If, но на деле IIf оказался не оператором, а функцией. А чтобы вызвать функцию требуется вычислить все её аргумент, т.е. потребуется вызвать все указанные функции в аргументах. Причем, IIf был так удобно подпихнут, что не все программисты улавливали сразу, что это не оператор, а обычная функция.
Суть оператор If в Visual Basic (да и в других языках) определять участки кода, которые должны выполняться в зависимости от условия. Попытка использовать IIf дало лишь поверхностное решение и не оправдало основного ожидания от неё – вызывать ту или иную функцию в зависимости от условия.
Так вот. Вернёмся к Go и к практике, которую предлагают для работы с ошибками. У меня, сложилось такое впечатление, что выше приведённая практика обработки ошибок очень напоминает IIf в Visual Basic. Расширим пример:
w := &errWriter{w: fd}
ew.write(getAB())
ew.write(getCD())
ew.write(getEF())
// and so on
if ew.err != nil {
return ew.err
}
Соответственно, здесь возникает вопрос: если в строке ew.write(getAB()) возникает ошибка, то почему выполняются getCD() и getEF(), когда в их результирующих значениях смысла нет?
Почему возникает такой вопрос? Дело в том, что ошибка — это значение, которое определяет, какой код должен выполняться. И это четко видно в первоначальном способе обработки ошибок в Go.
_, err = fd.Write(getAB())
if err != nil {
return err
}
_, err = fd.Write(getCD())
if err != nil {
return err
}
_, err = fd.Write(getEF())
if err != nil {
return err
}
Значение ошибки обрабатывается оператором if. И обработка значения ошибки в if как раз и определяет, какой код должен быть выполнен дальше. При чём в большинстве случаев для принятия решения, важно наличие ошибки, а не её содержания.
По моему мнению, ty-catch-finally как раз и занимаются тем, чем нужно, определяют код, который будет выполняться в зависимости от возникшего исключения.
P.S.: Тут вспомнилось из VB:
On Error Resume Next
Но лучше бы не вспоминалось. Чур меня, чур.
divan0
Пример в статье был подан не как «вот вам специальный IIf, чтобы хендлить вот такой случай», а как пример того, как смотреть на ошибки. Если понять посыл статьи, то всё становится намного проще.
В аналогиях важно видеть не только схожее, но и различное.
lair
Это не отвечает на вопрос «если в строке
ew.write(getAB())
возникает ошибка, то почему выполняютсяgetCD()
иgetEF()
, когда в их результирующих значениях смысла нет?»Кстати, на этом примере как раз хорошо видна разница между го-шным «ошибки — это значения» и монадой Try, которая тоже трактует ошибки как значения.
divan0
+3 статьи и перевода по Go :)
divan0
Представьте, что речь не про ошибки, и ответьте сами на этот вопрос :)
lair
А во всех случаях ответ один и тот же — потому что код написан неэффективно. Так зачем же предлагать заведомо неэффективное решение?
(кстати, это еще и не рефакторинг, вопреки тому, что написано в оригинальной статье)
jj_killer
Хотел уточнить, эффективно это ty-catch-finally для control flow?
lair
Зависит от реализации, очевидно. Может быть try-catch, может быть, композиция монад, может быть — тупая цепочка ифов.
jj_killer
Насколько я знаю концепция исключений для управления состоянием, в общем случае, считается антипаттерном. К этому, в частности, привело то, что во многих ЯП, отношение к исключениям, сама логика их работы и реализация менялась со временем. И как я понимаю, изначально маленькая команда разработки Go сознательно отказалась от этой концепции учитывая предыдущий опыт.
Реализация большинства монад хоть и проще стека исключений, все равно требует достаточного времени на тестирование и последующий «евангелизм» среди разработчиков.
Мне вот интересно, что осталось за бортом? Более практичное и функциональное, чем IF и менее сложное в реализации и отладке, нежели монады и исключения.
P.S. Последние несколько месяцев по работе имею доступ к одному большому проекту на C++, достаточно узкоспециализированному, но очень популярному в своей нише. Продукт очень недешевый, и позиционируется как надежный, но вот исключениями там совсем беда, и чем старее куски кода, тем все хуже. :(
divan0
В Google исключения не используются нигде, в том числе в С++, именно по озвученной вами причине.
TrueMaker
Ой не надо. Используются. Как минимум в 2010м использовались.
lair
Состоянием чего?
Ну вот в C# (и вообще в .net) отношение и логика не менялись никогда (насколько я знаю). Реализация внутри если и менялась, то это было инкапсулировано, и внешнее поведение оставалось неизменным.
Что сложного в реализации (и тестировании) монады Try в языке, в котором уже есть все необходимые механизмы? (конечно, если механизов в языке нет — как в Go — то это действительно проблема, тут не поспоришь)
Особенно учитывая, что делает (должна делать) это команда разработки самого языка (его базовой библиотеки), а никак не разработчики-пользователи.
jj_killer
Под состоянием я имел ввиду логику исполнения (control flow). С C# я работал достаточно давно, уж лет 8 назад, но тогда считалось, нерациональным использовать исключения для управления логикой из-за огромных накладных ресурсов и просадки производительности. Подозреваю, что сейчас это не так актуально, так как часто это встречаю в C# коде. В Java сама реализация исключений переписывалась, на моей памяти, как минимум два раза.
Собственно говоря, мой вопрос о чем-то посредине остался без ответа :( Тут ниже уже написали по макрос для Rust, интересная тема, но тут опять же, надо были изначально закладывать такую возможность.
lair
Тогда утверждение про антипаттерн в общем случае неверно. Исключения не надо использовать для управления логикой исполнения в нормативном сценарии выполнения (вместо условного
goto
), но нет ничего плохого в использовании исключений для сигнализации о возникновении ненормативного сценария выполнения (и переключения потока выполнения в этот сценарий).И сейчас так считается. Отсюда и правило, написанное мной выше. Действительно, исключения в .net ресурсоемки — но это не значит, что невозможно сделать нересурсоемкую реализацию исключений.
Для того, чтобы искать середину, надо установить границы. Я продолжаю не понимать, что сложного в реализации и отладке монады try.
Эмм, макрос
try!
— это монада Try (в Rust она выражена типомResult
) + синтаксический сахар (в скале — for-comprehension, в F# — computational expression). Вот вам приблизительно тот же код на F# (править под BCL не стал, извините):(хотя, конечно, макросы выглядят красиво, не поспоришь)
jj_killer
Хорошо, а как тогда запретить использовать исключения во всех остальных случаях? Собственно, насколько я знаю, причины отсутствия исключений в Go, это сложность реализации и желание ограничить область использования исключительно ошибками.
Полагаю при должной настойчивости и запасе времени все возможно, на практике же реализация исключений в ЯП одна из фундаментальных проблем. По вашим же ответам почему-то создается ощущение, что это если не пустячное дело, то говорить тут особо не о чем.
Более того, вот вы пишите, что до сих пор это считается порочной практикой в C#, а на деле это происходит достаточно часто.
Насчет монад, уже вроде решили, что сделать на Go так нельзя. Мне вот так сходу трудно здесь сделать какие-то выводы, почему так сложилось исторически, надо дальше копать.
lair
Никак (ну, кроме статического анализа кода, да и то).
В Go есть panic, который ведет себя «почти как эксепшн» (есть некоторые весьма существенные отличия, но я не знаю, определяющие ли они для того, чтобы запретить все лишние возможности).
Это не пустячное дело, но это реализуемо. Основная проблема исключений не в ресурсоемкости, а в тех компромисах, на которые мы идем. И вот как раз они-то и служат поводом для дискуссий.
С тем, что исключения более подвержены проблеме плохого программиста, я спорить не буду.
Монады были приведены к вашему упоминанию Rust, решение которого вам нравится — а монады, почему-то, нет.
jj_killer
К монадам у меня вполне индифферентное отношение. Но без доп. информации я не могу сказать почему этот вариант проигнорировали.
lair
В смысле «почему его проигнорировали в Go»? Потому что адекватные монады нельзя сделать без дженериков, а дженериков в Go нет.
jj_killer
Понятно, что нельзя, я вот нарыл такой док, в свободное время прочитаю, потому что тема с дженериками постоянно всплывает, и опять же, верить на слово никому нельзя.
vintage
Так покрасивше всё ж:
fun fileDouble (path: string) {
return 2 * path.read.trim.parse!int
when FileNotFound return 0
}
lair
Даже не буду спорить. Чем обеспечивается функциональность (я, к сожалению, на глаз язык не узнал).
vintage
В данном случае это Go + D + [немного фантазии].
kekekeks
Ogoun
Ради интереса сделал тест для шарпа:
Для Release получаю:
Для Debug (без оптимизации):
По результатам, повода отказываться от использования этого подхода в шарпе не вижу.
kekekeks
Теперь прогоните на Mono (желательно под Linux/OS X) и удивитесь.
Ogoun
К сожалению нет возможности, работаю только под win. Но если кто-нибудь запустит и покажет, с удовольствием удивлюсь.
Prototik
lair
Я, наверное, чего-то не понимаю, но в вашем коде же во время exception не будет брошен ни разу?
Ogoun
Именно так, добавил на всякий случай для компилятора
lair
Тогда неудивительно, что вы не видите разницы: exception несет (по крайней мере, в нормативной реализации в CLR) ощутимые накладные расходы именно при бросании/обработке. У меня есть реальный пример в опыте, когда одно криво написанное место с эксепшном тормозило обработку пятимегабайтного пакета данных в тридцать раз.
Ogoun
Но тем не менее, в стеке вызовов все равно создаются фреймы для реализации блока try catch. По сути я смотрел накладные расходы именно на оборачивание, а не на обработку исключений, т.к. выброс исключения ситуация исключительная, программа в продакшене должна работать без них, а если они случаются, то затрачивание лишних, пусть даже секунд, на их обработку, наименьшая проблема.
lair
Вот это как раз и корень всех дискуссий о ресурсоемких исключениях: поскольку они ресурсоемки, их нельзя использовать всегда и везде, и начинаются споры, где их применение обосновано, а где — нет.
(заодно еще, конечно, синтаксис обработки нелинейный, но это свойство не только ресурсоемких исключений)
jj_killer
Мне трудно дать какие-то комментарии, так как уже много лет не использовал язык, но лет 8 назад картина была бы совершенно другой, что еще раз показывает насколько тема непроста.
P.S. В C# разве нету встроенного класса для бенчмарка?
Ogoun
Есть, но слишком простой тестик чтобы захотелось ими пользоваться. Stopwatch'у вроде можно доверять в таких замерах
Из этой статьи
divan0
Вы же не притворяетесь, правда?
Код в статье был примером, служащим для демонстрации подхода.
Продолжайте вашу борьбу с мельницами дальше :)
Cim
Понятно, что в статье был пример того, что с этими ошибками можно сделать и как на них смотреть.
Но тем не менее, ответа на вопрос «что делать с кучей повторяющихся `if err != nil { return err }`» нет.
Ошибки в Go — это даже не ошибки, это просто «нафиг нам ошибки, пусть программеры обрабатывают значения, а еще мы сделаем возможность удобненько возвращать несколько значений из функции и всем объясним, что код ошибки должен быть вторым значением». Немного форсировали обработку ошибок тем, что код не скомпилируется, если не присвоить все возвращаемые функцией значения переменным или оставить неиспользуемые переменные.
Как по мне, с одной стороны го — это архитектура минимализма. С другой — всем было тупо лениво и и так сойдет, а программеры потом задолбаются писать один и тот же код тысячу раз и сделают вообще внешние утилиты-генераторы, а нам вообще впадлу думать и впихивать генерацию кода на уровне компилятора.
divan0
Есть же ответ. Есть это действительно «куча повторяющихся» — сделайте то же, чтобы вы делали с «кучей повторяющихся» кода, если бы речь шла не про ошибки. В этом главная суть.
Это не код ошибки, вы упускаете фундаментальную разницу.
faiwer
В большинстве случаев, когда речь идёт не об ошибке, куча повторяющегося кода выносится в отдельный метод и вызывается по мере необходимости. Но т.к. из отдельного метода нельзя сделать двойной return, то, если таковой блок его требует, обычно, приходится сильно извращаться. Благо такая ситуация возникает очень редко, и зачастую её можно обойти, написав код иначе. Если же нельзя, приходится писать мало вменяемое нечто с грустными глазами.
А что мы имеем тут? Есть груда повторяющегося кода, которая требует немедленного выхода из метода. Вынести её куда то возможности нет. Переписать код так, чтобы её избегать, насколько я понимаю, тоже нельзя, потому что если метод требует обработки ошибок, то он её требует, откуда его не вызывай.
В общем ситуация грустная. Не так давно вышла статья про похожую ситуацию в Rust, и там я увидел использование макроса Try!
Не сказать, чтобы это решение выглядело изумительным, но хотя бы код, написанный с её применением, начинает напоминать некую бизнес-логику, а не груду копи-пасты.
Приведённый вами метод «обхода» проверок (пусть и в качестве примера), как справедливо замечает топик, не выдерживает вообще никакой критики. Мало того, что он безмерно уродлив (да-да), так он ещё и безмерно избыточен.
divan0
Вот вы справедливо заметили про «такая ситуация возникает очень редко». «Груда повторяющегося кода» — это признак плохого кода. Про циклическую сложность слышали? В Go есть даже линтер, которые показывает функции, в циклической сложностью больше 10. Если у вас 3 или 4 вызова, в котором вам нужно проверить ошибку — ничего тут страшного нет, зато любой, кто будет читать код после вас, сразу будет видеть, как идет flow и что происходит в результате ошибки.
Cim
это не важно: код ошибки, структура, строка или «ололо пиу-пиу упячка». Есть договоренность, как в питоне о том, что методы, наичнающиеся с двойного андерскора считаются приватными. В Го теперь договоренность., что вторым параметров возвращается ошибка. Да, я понимаю, что обрабатывать ошибку надо на общих основаниях, в общем-то. На общих основаниях с значениями. И как в каком-нибудь руби или яве нельзя(т.е. можно, но это совсем дискредитирует логику эксепшенов) 100 строк кода обернуть в один единый `rescue`, а надо делать ветвления(`rescue IOError ...`, `rescue ActiveRecordDamnExceptionPewPew...` etc), также и в Го нельзя взять и родить один простой «иф», а каждая ошибка будет решаться в индивидуальном порядке.
Исключительно синтаксические примеры выглядят по дебильному:
err := write(a);
if err != nil { return err }
err := write(b);
if err != nil { return err }
err := write;
В реальной же жизни, как и в случае с эксепшенами, будет как-то так:
user, err := find_user_by_id(user_id);
if err == 33 { write(«User not found»); return }
if err == 55 { write(«OMG can not connect to db, call admin! panic! aaa!»); notify_rollbar(...) }
transaction_id, err := update_user_in_transaction(user, params)
if err != nil { rollback_transaction(transaction_id); return }
Безусловно, в реальной жизни тупых синтетических блоков `if err != nil { return }` нет. Точно также не было бы и «единого центра обработки эксепшенов» в какой-нибудь яве или руби т.к. каждый метод кинет свой эксепшен и по хорошему, надо их обработать индивидуально(херачить ветвление), а не заглушить всё самым базовым классом Exception.
Однако это, епрст, не отменяет моего другого ощущения: «wtf???». В данном случае, это wtf исключительно про «договоренность» о том, что ошибку надо бы, по хорошему, возвращать вторым(последним) аргументом. Чтобы стандартно и красиво было, чтобы все как один, чтобы пользователю функции не приходилось гадать. И вот эти вот договорняки меня и расстраивают, в общем-то. Ну еще расстраивают и некоторые другие штуки в го, но они к данной теме не относятся.
divan0
Ну это проходит, когда вы встречаетесь с кодом на эксепшенах, где ошибки вообще не обрабатываются как следует, просто потому что им не уделяют должное внимание, полагаясь на «механизмы языка». Это как раз причина, по которой исключения не используются в Google даже в С++.
kekekeks
Вот не надо гугл в качестве положительного примера приводить, от использования некоторых их либ, в частности, Skia, крайне негативный опыт. Зачастую в качестве возвращаемого значения приходит null вместо объекта без какой-либо детализации произошедшего. В итоге разбираться в причинах произошедшего приходится построчной трассировкой их кода с GDB наперевес.
psylosss
И тут РНР обскакал Go :-P
lair
И этот пример как раз демонстрирует неэффективность.
А как же получить одновременно эффективное (т.е., без избыточных вызовов) и при этом немногословное (т.е., без кучи if-ов) решение?
divan0
Этот пример демонстрирует подход к проблеме и ход мыслей. Остальное — надуманное вами лично.
lair
Как получить одновременно эффективное (т.е., без избыточных вызовов) и при этом немногословное (т.е., без кучи if-ов) решение?
divan0
Вы меня, конечно, порядком, достали, но отвечу.
Итак, вы взяли почти синтетический пример из одной статьи, на который придумали модификацию в этой статье, и который вы теперь подаёте как «демонстрацию неэффективности языка», и просите меня написать за вас код, как будто это изменит ваше непробиваемое желание доказать, насколько плох язык, который вы даже не знаете. Вопрос о практичности примера пока оставим в стороне.
Раз уж вы так хотите использовать в повторяющихся вызовах write() какие-то функции в качестве параметров, но не хотите, чтобы они вызывались в случае ошибки — сделайте так, чтобы они в этом случае не вызывались! Это же просто. Функции это такие же значения, и во write можно передавать саму функцию, а не ее результат, и запускать функцию после проверки на err. Кроме того, если вы уже настолько синтетически придумали пример, в котором лучше всего напрашивается выбрасывание «исключения» — используйте panic/recover! Go не запрещает это делать, он специально для этого в языке, чтобы использовать тогда, когда это действительно необходимо и оправданно.
lair
То есть сигнатура
write
меняется с «обычного» значения на функцию? Или добавляется новый метод с другим типом входного значения?(я даже не буду спрашивать, что делать, если написанный по такому паттерну код мне неподконтролен)
Заметим, вы убрали только одну неэффективность из двух — вызов функции, поставляющей данные для
write
, но вот сам (избыточный) вызовwrite
никуда не делся. Надеюсь, его накладная стоимость не очень велика.Я ничего не придумывал, это пример из вашего поста.
divan0
Это модифицированный пример из поста, который я переводил.
lair
Его модификация не выходит за границы обычного ожидаемого сценария. Но самое главное, что поведение-то точно так же меняется и на исходном примере, просто там это менее очевидно.
Stronix
Для данного условия можете воспользоваться panic() и recover() play.golang.org/p/vomyyTus6a
lair
А-га.
Возьмем, значит, исходный код:
и чуть-чуть его перепишем в демонстрационных целях. Надеюсь, никто не будет спорить, что семантика полностью сохранилась?
Вот (ожидаемый) вывод:
Окей, перепишем как предлагает Пайк (я добавил отладочный вывод внутрь
errWriter.write
, чтобы было видно происходящее). Результат:Тоже предсказуемо, для внутреннего объекта последовательность вызовов не изменилась, но снаружи их стало больше. Не ужас, но если снаружи есть тяжелые вычисления, может быть неприятно, поэтому, как вы предлагаете, воспользуемся
panic/recover
.Пишем «в лоб»:
Вывод ожидаем:
Но, кажется, я ненастоящий гофер: сымитируем панику — и вывод становится неожиданным:
Ну да, никогда не заглушайте ошибки. Следующая итерация работает почти как надо. Единственное отличие в том, что теперь паники выводятся как
panic: Why? [recovered] panic: Why?
и, кажется, потерей коллстека (а точно нет аналога
throw;
, может я просто искал плохо?).Но чуял я, что-то не так. И правда: теперь паники изнутри, имеющие честный интерфейс
error
, будут ловиться, хотя это не то поведение, которое нам нужно. Но с другой стороны, как чинить, тоже понятно.Ура! Работает (с теми же оговорками про изменение паники). Если кто видит edge case, который я по неопытности просмотрел — исправляйте.
У меня только один вопрос: а чем это лучше
try...catch
?powerman
У меня только один вопрос: нафига писать такую дичь, полностью оторванную от реальных задач?
lair
Посоветовали использовать
panic
для решения проблемы, описанной в посте. Я неправильно понял совет? Как надо было?poxu
А покажите пожалуйста пример хорошей обработки ошибок в Go не оторванный от реальных задач.
Stronix
Потому, что r.(error) вернёт nil. play.golang.org/p/zU6qWnI0kf
lair
Да я знаю, почему. Легче не становится.
Stronix
Ну, по крайней мере, дальнейшие варианты уже не имеют смысла совсем.
lair
Какие «дальнейшие варианты» и почему не имеют смысла?
Stronix
Кстати, прочитав статью blog.golang.org/errors-are-values, я так и не понял, чем не устроил вариант Пайка? play.golang.org/p/Y5AQy10QNU
lair
Приблизительно вот этим:
Если в методе есть тяжелые вычисления, то это… обидно.
Stronix
Тогда опять же, почему не вариант с panic/recover?
lair
Ну вот тут рядом пишут, что это дичь какая-то. Мне он, простите за вкусовщину, тоже кажется уродливым.
Stronix
Как мне показалось, это относилось не к моему примеру.
lair
Ваш пример не решает задачу, поставленную в посте — поведение функции меняется.
nwalker
Вычисления — это еще что. А если вызываемый метод неявно меняет какой-то стейт?..
lair
Это уже, будем честными, и без обработки ошибок не самая лучшая практика.
nwalker
Да, само собой. Но тем не менее, гарантий-то никаких нет.
catnikita255
Заминусовали бедного.
marapper
Астрологи объявили неделю ошибко в Го на диване. +4 поста.
corristo
«Errors are values» в Go завезли, удобного средства композиции всего этого — нет. Для примера удобного средства см. Rust с его Result или Haskell с его Either. Обидно, что особенности Go создают у людей предвзятую картину относительно такого подхода к обработке ошибок.
stepanp
Чего всем так не нравятся «Errors are values»? Неужели нравится бесконечные лесенки из трай-кетчей городить?
lair
Не нравится. Но цепочки
if
-ов нравятся не больше.divan0
Мне не нравится механизм эксепшенов, но я не хожу во все посты про языки, где он используется и не рассказываю, как он мне не нравится.
lair
Если вы не заметили, мы сейчас в посте про обработку ошибок в Go. Не вижу ничего странного в том, чтобы обсуждать в нем… обработку ошибок в Go.
divan0
«Обсуждать» и рассказывать месяц за месяцем, как вам не нравится то, чем вы не пользуетесь — это разные вещи.
Обсуждение ценно, когда собеседник знает предмет обсуждения.
lair
Да вроде бы в посте все написано про возможности Go в этом вопросе. Что еще нужно знать про ошибки в Go (которых там нет, а есть значения, я помню), чтобы их обсуждать?
namespace
Конечно не рассказываешь, потому что тебя заклюют. Эксепшоны — это известная common practice, Go тут в оппозиции.
powerman
Я вот не могу понять одну вещь… в статьях критикующих стандартный способ обработки ошибок в Go почему-то подразумевается, что всем
умным людямопытным разработчикам понятно, что этот кодужасенплох. Но при этом я пока нигде не видел аргументированного объяснения, что же конкретно в нём плохо. Можете пояснить этот момент?Да, он выглядит громоздко — по 4 строки на одну операцию. Но обычно проблема громоздкого кода в том, что его сложно читать. В данном случае это не так — такой код читается очень легко, конструкции типовые, есть всего 2-4 варианта которые обычно используются, и которые со второго-третьего дня работы с Go начинаешь моментально распознавать.
Да, там дикий копипаст. Но проблема копипаста не в copy или paste, а в том, что дублируется логика, копируются баги которые потом нужно исправлять в куче почти идентичных мест, etc. — но в данном случае, конкретно с копированием этих трёх строчек — таких проблем не возникает. У нас вроде не секта, чтобы безусловно кричать «копипаст не пройдёт» или «goto это опиум для народа» — существуют вполне конкретные причины почему использование копипаста или goto вызывает проблемы, и существуют ситуации в которых эти причины не актуальны или менее важны чем польза от копипаста или goto.
Да, это типовой код, практически 100% шаблон. И его лениво каждый раз набирать. Ну так а кто заставляет его набирать? Для таких вещей в текстовых редакторах существует поддержка snippet-ов. В vim, например, достаточно нажать 5 кнопок
errn<Tab>
чтобы вставить в код этот if на 3 строчки.Итого лично у меня получается следующее: код легко читать, быстро набирать, проблем с ним не возникает (в отличие от вышеупомянутых случаев, когда этот код пытаются «соптимизировать» засунув во вспомогательную функцию-хелпер, которая обладает целым букетом недостатков и крайне сомнительными достоинствами), единственная более-менее адекватная претензия — он не компактный. Но лично у меня 18 лет опыта работы с Perl, у которого с чем уж точно нет проблем, так это с компактностью кода… И на мой взгляд, хотя компактность кода иногда улучшает читабельность, гораздо чаще она её катастрофически ухудшает. А претензий к читабельности кода на Go лично у меня нет, так что возникает вопрос: а нужна ли на самом деле эта компактность кода? И для чего она нужна, если не для улучшения читабельности кода.
lair
Лично мне это читать тяжело. Я воспринимаю эти повторы как смысловой шум. Если я хочу понять бизнес, происходящий в коде, то я должен научиться отфильтровывать эти конструкции — но если я научусь это делать, вероятность того, что я пропущу в них что-то важное, резко увеличивается.
(к копипасте то же самое относится) чем больше таких повторов (при минимальной вариативности), тем больше вероятность, что глаз замылится и не увидит, что в каком-то случаев применен другой вариант. Это специфика монотонного кода, глаз устает и перестает воспринимать происходящее.
Впрочем, лично у меня к «стандартному способу обработки ошибок в Go» есть существенно более фундаментальная претензия, состоящая в том, что он… не стандартный. В стандартной библиотеке (если я, конечно, не путаю, что там стандартная, а что — нет) Go используются как минимум три разных способа сообщения об ошибочной ситуации (я не считаю
panic
), а обсуждаемый выше пример с функцией-хелпером — это пример, приводимый в официальном блоге Go одним из авторов языка, и там, в итоге, описывается четвертый способ обработки ошибок, применимый к классу из стандартной библиотеки.Нет, несомненно, «ошибки — это значения», и поэтому к ним применимы все языковые средства во всем их разнообразии… вот только униформность в этом случае сильно страдает, а вместе с ней начинают рассыпаться и приведенные вами аргументы.
powerman
Это очень плохо — код, безусловно, выглядит намного проще и понятнее если из него выкинуть обработку ошибок, но дело в том, что обработка ошибок это важнейшая часть логики приложения, и даже бизнес-логики. Поэтому логика обработки ошибок должна быть максимально наглядной и легко доступной, т.е. находиться на виду, рядом с тем местом где ошибка возникла. Как и рекомендуется делать в Go.
А вот здесь всё совсем наоборот. Фильтруются только типовые конструкции вроде
if err != nil { return err }
— за любое минимальное отличие в этой конструкции глаз сразу зацепляется, что сильно уменьшает вероятность пропустить что-то важное. Возможно, конечно, что здесь тоже имеют место отличия между нашими личными особенностями восприятия, но дело может быть и в том, что я на Go пишу, а Вы (как я понял из комментариев, прошу прощения если я ошибся) — нет, так что я опираюсь на практический опыт, а Вы на теоретическое впечатление сложившееся от чтения статей по Go. Раз уж Вас настолько интересует Go, что Вы постоянно критикуете его в комментариях, может быть Вам стоит потратить несколько недель и попробовать его в реальном проекте?Стандартный способ обработки ошибок в Go — библиотеки не должны выкидывать наружу panic за исключением действительно редчайших особых ситуаций, а должны возвращать ошибку как значение. Покажите мне, пожалуйста, конкретные примеры кода стандартной библиотеки, о которых Вы говорите… хотя я подозреваю, что Вы просто подразумеваете что-то иное под «стандартным способом обработки ошибок в Go».
Каждый может проявить слабость если сильно задолбать. Наверняка есть ситуации, когда такой хелпер вполне уместен — как и в случае с копипастом/goto у него есть и достоинства и недостатки, и существуют ситуации в которых достоинства перевешивают. Но я пока таких хелперов в реальном коде не встречал, так что эти ситуации скорее всего слишком редки, чтобы их имело смысл обсуждать в контексте глобальных особенностей языка.
Извините, я, наверное, сильно устал, но я не уловил логики в этом утверждении. Можете развернуть мысль подробнее?
lair
Это говорит человек, который в начале треда написал «лично у меня получается следующее». Я, несомненно, с удовольствием посмотрю на объективную статистику по читаемости на больших выборках. Но пока мне греет душу одно: про приведенный в посте код сам Пайк пишет:
А вот после рефакторинга:
На мой взгляд, это описание совпадает с моим. То же самое относится к следующей вашей реплике:
Я не согласен с вами в том, что инфраструктурные ошибки (ой, у нас сервер не ответил) — это часть бизнес-логики. Чем больше ситуаций вам надо обрабатывать, тем хуже видна достигаемая бизнес-цель на фоне обработки побочных, никому не интересных вещей.
Я уже говорил: у меня нет реального проекта, на котором я могу попробовать Go.
Вы неоднократно говорите, что проблемы, вызванные повторяемостью кода, решаются привычкой и шаблонами. Но если в реальности вариантов обработки ошибок не ровно один, а больше (мы сейчас к этому вернемся), то и привычки сбиваются, и шаблоны писать все сложнее.
(1)
(2)
(3)
Бонус-трек:
lair
Извиняюсь, вот правильный код для бонуса (чтобы консистентно было):
namespace
Это все уже много раз обсуждалось. На самом деле, разработчики Go совсем чуточку обосрались. Оказывается, errors are values недостаточно. Вот какое шапито… те ошибки, которые Go не считает «исключительными», на самом деле, почти всегда cебе очень даже и исключительные. Не смогли записать в файл, не смогли открыть сокет с базой, не смогли то-се — это все исключительные ситуация, такого быть не должно. Валидация и тд — там все просто: да/нет, либо структура с ответом (error!). В принципе, ничего плохого в errors are values ошибках нет, учитывая тот факт, что они совершенно бесполезны (ошибки — это строки). В среднем случае, это все пишется в лог (без контекста и стектрейса, лол), а в лучшем — разраб вручную апкастит error до кастомного типа ошибки и кое-как вручную туда запихивает контекст и stacktrace.
Но я уверен, Пайку виднее.
powerman
Описываемый Вами стиль оформления ошибок в виде сложной структуры, как и любое абстрактно-универсальное решение, не является уместным везде и всегда. Там, где это имеет смысл — стандартная библиотека Go возвращает такие структуры (без контекста и стектрейса, там, где они нужны их несложно добавить:
panic(err)
). В абсолютном большинстве задач, с которыми работал я — текстового описания ошибки было абсолютно достаточно. и оно было намного удобнее сложной структуры. Желание всё усложнять без необходимости обычно проходит примерно через 10-15 лет работы программистом.P.S. Кстати, поздравляю, Вы с этим шапито и немотивированными наездами на Пайка сформировали достаточно уникальный стиль речи, по которому автор текста определяется не хуже, чем по текстам Мицгола.
namespace
Ну смотри, ты не прав. Я считаю, что не стоит разделять на конечные пкг и приложения, не стоит этим заниматься. Go у нас очень открытый и черных ящиков нет — конечному приложению нужны стектрейсы, если что-то в библиотеке сломалось. Скажем так, я не вижу причины не использовать сквозные паники.
powerman
Почему, собственно?
Различать разные сущности очень полезно — это позволяет каждую из них обрабатывать максимально специфичным для неё, т.е. более простым и эффективным, способом. Абстрактный и универсальный код — это в большинстве случаев либо тривиально и практически бесполезно, либо очень сложно и медленно.
Насколько сильно сломалось? Если очень сильно — и так будет паника. Если библиотека просто некорректно работает и вернула не то значение или ошибку — в её коде всё-равно придётся разбираться, отлаживать и фиксить, и поиск места где эту ошибку вернули (на которое бы указал стектрейс, и то только в случае возврата ошибки-исключения, а если библиотека просто возвращает некорректное значение то никакого стектрейса бы не было всё-равно) обычно занимает секунды/пару минут, которые полностью теряются на фоне времени необходимого на отладку и исправление кода (да и это время не является бесполезно потраченным — чтобы исправить ошибку всё-равно нужно разобраться почему она возникает, т.е. вычитать этот же самый код).
Ну что тут скажешь… беда, просто беда. А я вот не вижу причины себя ими ограничивать, предпочитаю использовать тот подход, который лучше подходит в конкретной ситуации.
namespace
Я тебя понял, короче говоря.
NeoCode
А я думаю вот что.
Мне не нравятся исключения в существующем виде тем, что кроме вызова функции необходимо еще городить try-catch, причем если возвращаемое значение доступно из прототипа функции, то какие там функция выбросит исключения — тайна, для раскрытия которой нужно или читать документацию (если она есть и актуальна) или изучать код функции и всех функций, которые она вызывает. То есть — неявность.
С другой стороны, бесконечные if-else тоже не лучший вариант.
Откуда вывод — необходимо придумать что-то еще, совмещающее достоинства обоих методов и лишенное недостатков.
И вот какие мысли. Основной недостаток исключений — неявность. Поэтому сделаем их явными. Для начала, использование/неиспользование исключений в данном конкретном месте — это дело программиста. То есть если программист заключает функцию в try-catch с обработкой конкретного типа исключений — то этот тип исключений может генерироваться в данном коде. Иначе — не может, и должен генерироваться код возврата.
Хорошо бы в заголовке каждой функции обязать прописывать исключения, которые она в принципе может генерировать.
Принцип «ошибка это значение» тоже сохраняется. Для этого должен быть оператор, аналогичный return, но умеющий вместо возврата кода ошибки выбрасывать исключение, при его разрешенности в данном контексте вызова.
И наконец, переход от распространения исключения к возврату ошибки (хотя это самое сомнительное, но ладно — напишу): для этого, при отсутствии явных блоков try-catch, каждая функция является неявным блоком try-catch, превращающим любое исключение внутри себя в возврат кода ошибки по умолчанию для данной функции. Со всеми фичами исключений вроде раскрутки стека.
Выглядеть это должно так.
1. по умолчанию программист вызывает любую функцию, будучи уверенным, что она не выбросит исключений, а всегда вернет код возврата.
2. если программист хочет чтобы функция выбросила исключение и он готов его обработать, он просто разрешает раскрутку этого исключения для каждой функции, входящей в цепочку вызовов, до того места где исключение ловится; и второе — перед вызовом функции используется специальное ключевое слово (то же try), говорящее что мы готовы принять и исключение в том числе. Это наглядно, никаких неожиданностей — написано int x = try foo() значит мы не просто вызываем функцию, а «пытаемся вызвать, понимая что может и не получится». Городить catch при этом не нужно: если функция foo() вывалится с исключением, а у нас нет на нее catch — сработает catch по умолчанию, который конвертирует это в код возврата ошибки для данной функции. Исключение может распространяться дальше только в случае если мы разрешили это, указав в заголовке нашей функции что она может генерировать исключение такого типа.
Пока еще не во всем уверен до конца, но как-то так…
powerman
Большая часть проблем связанных с обработкой исключений вызвана тем, что ошибки и исключения смешивают в одну кучу.
lair
Мне очень неловко повторяться, но я, наверное, все-таки еще раз напомню про монаду Try. Она, конечно, недостатков не лишена, но вот уж явности ей более чем хватает (при этом
if..else
ей не обязательны).Это к тому, что есть больше двух способов работать с ошибками и исключительными ситуациями.
yar3333
Здравствуй, Java!
NeoCode
А в этом есть что-то плохое? (не писал никогда на Java, но на C++ реально напрягает когда не знаешь что там выкинет функция)
yar3333
Как и всегда: что-то получаешь, что-то теряешь. Где-то на Хабре была статья как раз на тему того, почему это плохо: при большой вложенности вызовов приходится либо таскать эксепшенов (что громоздко), либо махнуть рукой и указать при объявлении метода, что он выкидывает обобщённый эксепшен (что сводит плюсы на нет).
dbelka
Тут случай, когда макросы выручают. Спасибо Rust :)
defuz
Можно еще через and_then определить:
nwalker
Привет, монада Maybe, записанная в явном виде.
arvitaly
Классическая проблема терминологии. Если продолжать называть значения «ошибками», то и восприниматься они будут как ошибки.
А возврат множественных значений — это не ошибки, в асинхронных языках это всего лишь один из вариантов результата выполнения функции.
Просто с асинхронностью появляется проблема чтения кода, известная под названием «callback hell». А в go можно также линейно продолжать читать и писать код.
arvitaly
А с исключениями мы делаем предположение, что результаты разных функций будут совпадать (выдавать обобщенные исключения). Только при этом условии они нужны для возврата значения. А второй вариант использования — любое обобщение внутри одной функции, вроде пресловутого множественного elseif из статьи. Но обычно, это сигнал о нарушении SRP и необходимости создания новой функции, а не сигнал к созданию раздела done.
С пробросом вверх сразу возникает вопрос, какой степени обобщенности должно быть исключение, чтобы его смог разобрать кто-то выше чем непосредственно вызывающая функция. И в итоге приходим к тому, что либо мы обобщаем любые исключения до panic, warning, notice, deprecated, либо в обязательном порядке обрабатываем все варианты ответа функции непосредственно в месте вызова (как когда-то пытались сделать в Java с помощью throws), и как сделали в go более логичным образом.
No way.
P.S. Ах да, есть еще вариант обработки исключений специальным модулем, отдельно от основного потока. Но опять же это должны быть очень-очень обобщенные исключения, т.е. panic.
milast
В Go был выбран метод panic/recover/defer для обработки ошибок вместо try/catch по той причине, что он более понятный. Try/catch часто неправильно используют даже опытные программисты, т.к. путают и перемешивают ошибки и исключения, не говорю уже о неопытных и начинающих программистах.
В каком случае невыполнение SQL-запроса — это ошибка, а в каком обычное исключение?
В Go такой путаницы нет.
lair
Хм, а как Go рекомендует трактовать невыполнение SQL-запроса?
arvitaly
Как один из вариантов результата выполнения функции exec, например.
lair
И чем это отличается от (подставь свой язык)? В .net, скажем, ошибка при выполнении SQL-запроса — тоже «один из вариантов результата выполнения» соответствующего метода.
arvitaly
Отличается местом обработки результата, в go нельзя обработать результат выполнения функции непонятно где. В C# можно исключение ловить хоть в функции main, если речь об однопоточном приложении.
lair
Да, можно просто проигнорировать его.
arvitaly
Просто нельзя, только явно.
lair
«Просто» тоже можно, если «другие результаты» функции не интересуют. Пример тут в посте есть уже.
arvitaly
Не вижу примера, где можно не получить все результаты функции при ее вызове. То, что можно после вызова проигнорировать, это уже на совести (ответственности) вызывающей стороны и не менее явно чем try{ a(); }catch(Exception e){}
lair
Я же говорю: если другие результаты вызова не интересуют. Например, если вы вызываете
UPDATE
в SQL. Или вот еще более милое:arvitaly
Под результатами функции я понимаю, именно множественные результаты одного вызова функции. Т.е. априори, в программировании практически не используются действительно чистые функции и под «функцией» подразумевается часть функциональности системы, а не отражение входного множества на выходное. Поэтому, в go не стали продолжать тянуть лямку бессмысленной затеи с одним результатом, как в других языках. И исчезла потребность в исключениях. Единственный аргумент за один результат функции был принцип SRP. Только понимали его не правильно из-за неправильного определения «функции». В математике задача функции — получить одно выходное значение на одно входное. В других областях задача функции — выполнить часть работы в своей области ответственности.
На самом деле, go — это только начало, следующая ступень, отказ от стека. Тогда мы вернемся к процедурам, по типу объектов EventEmitter, когда функция бросает исключения, но не как результат для вызывающей функции, а как бы во внешнюю среду. Тогда и с асинхронностью (читай, многопоточностью) проблем не будет. Вопрос только, как это адекватно описать, чтобы человеческий мозг не ломался при чтении.
lair
Я тоже.
Если я выполняю
UPDATE
, и мне не важно, сколько строк обновлено, я могу просто проигнорировать весь возврат.Эмм, а как же функциональное программирование?
Программирование на continuations? Программирование на акторах? Имя им легион давно.
arvitaly
Покажите, как вы его проигноируете, вызовите, пожалуйста, функцию?
Ну вот только для математических задач функциональное программирование и подходит, во всех других сферах функция обязательно имеет зависимости (внешние системы), а значит не может гарантировать один и тот же результат при одинаковых входных значениях. Отсюда return null, throw new Exception и т.д. Хотя действительным результатом выполнения функции будет куча вызов внешней системы.
Ну сейчас нет времени честно обсуждать каждый вариант, но я пока достойной реализации еще не видел, ИМХО.
lair
Вы это серьезно? А вы не пробовали декомпоновать вашу задачу таким образом, чтобы весь необходимый ввод от «внешних систем» получался отдельно, а потом приходил в вашу функцию входным значением?
Знаете, в моей жизни было много функций, которые возвращали
null
или бросали исключения, при этом не имея никакой внешней зависимости. Даже у стандартных алгоритмов бывают недопустимые ситуации.arvitaly
Ну для разработчика go этот код равносилен пустому catch. Если вы не считаете это достаточно явным указанием на игнорирование результатов выполнение функции, я скажу, что вы просто привыкли к другому синтаксису.
Под внешней системой я подразумеваю не файловую или сеть, а любую зависимость нашей функции. Количество чистых функций исчисляется единицами. Если же где-то вызвать все зависимости, получить результат, то это и будет 99% работы, и да, можно создать чистую функцию для компоновки конечного результата, это и будет математическая функция, оперирующая примитивами. Однако, сложность как раз вызвать все эти зависимости и собрать всевозможные результаты.
Ну это и говорит о том, что даже без зависимостей, концепция возврата одного результата не работает. Нужны механизмы для описания функции с множественными результатами с разными типами (FileNotFoundException, «file content», null).
lair
То есть разработчик Go наизусть помнит, какие функции возвращает ошибку, а какие — нет, и где такой вызов эквивалентен пустому catch, а где — нет?
Никакой особой сложности, по большому счету. Собственно, это экстремальное применение DI.
Вы так говорите, как будто union type — это что-то совершенно уникальное и недостижимое. А то, что вы сейчас описываете — это типичный
Try[Option[T]]
.arvitaly
Претензия не по существу. Смотреть документацию (или сигнатуру) функции перед ее использованием признак профессионализма, или трата времени, ведь если что, кто-то где-то бросит Exception, который кто-то где-то поймает? И разработчику go не нужны эквиваленты из других концепций.
Особой сложности вообще нет, есть просто сложность, включающая число сочетаний результатов вызовов зависимостей (будь то Exception или return). И где-то нужно ее описать, хотите в DIC — ваше право, суть не меняется.
Union type не интересен, повышает сложность и чтения и рефакторинга в разы, лучше уж сразу слабую типизацию.
Try — костыль, желание оставить один результат выполнения функции, зачем? Это обман разработчика мнимой чистотой функции.
lair
Код существенно чаще читается, чем пишется. Когда я читаю (бегло) код, я не хочу лезть смотреть сигнатуру каждой используемой функции. Я могу на глаз определить, проглочена ошибка, или нет?
Все та же система типов плюс их композиция (предпочтительно, конечно, монадическая)
Каким именно образом он повышает сложность? Можете на примере показать?
Чтобы явно указать, что функция может возвращать значение с семантикой «ошибка».
Почему мнимой? Как вообще тип возвращаемого значения связан с чистотой?
arvitaly
А вы можете на глаз определить, выкидывает функция Exception или возвращает null? Если да, склоняю голову. Каждый раз и не нужно лезть, нужно просто знать, либо лезть (чаще всего навести мышку), если память уже не позволяет.
Ну все просто, как я уже сказал, функция может иметь действительно разные результаты (например строку или объект ошибки), и union type будет таким, что пересечение будет минимальным (toString?), и мы возвращаемся к полному набору значений из обоих результатов, либо невозможности его реально использовать. А зачем нам постоянно описывать этот тип, если можем просто указать, что может вернуться либо то, либо то?
Для меня это означает, что функция может возвращать 2 разных значения, а «семантика ошибка» — игра слов, скрывающая это.
Ну чистая функция для одного входного значения всегда вернет то же самое выходное. А здесь она еще оказывается может вернуть ошибку, значит она не чистая, как бы мы не пытались костылями это прикрыть.
lair
В языках с исключениями полезно считать, что любая функция может их кинуть (это, в принципе, так). С null сложнее, но там можно приручить статический анализатор.
Извините, но union type — это объединение всех результатов.
Ну так и делается:
Either[string,int]
Вы не понимаете. В общем случае, Try — это частный случай Either, в котором один из типов — это подтип Error. Поэтому Try всегда говорит, что может вернуться или ошибка, или осмысленное значение.
Функция, возвращающая Try — тоже.
Кстати, а мы тут точно не путаем чистые функции с детерминированными? Для дискуссии это не очень важно, но любопытно стало. Я считал, что чистая функция — это функция без побочных эффектов.
Ошибка может быть реакцией на конкретный подвид входного значения. Например, у нас есть функция, которая считает кратчайший путь по направленному графу с весами. Есть алгоритм, который не умеет обрабатывать графы с отрицательными циклами. Соответственно, для такого алгоритма очень логично возвращать одно из трех: (а) кратчайший путь (б) никакого пути, если его в графе нет (ц) ошибку, если на пути встретился отрицательный цикл. Значение строго детерминировано входными данными, но при этом прекрасно покрывается семантикой
Try[Option[Path]]
.arvitaly
В языках без исключений полезно считать, что функция может вернуть ошибку.
Извините, для использования, это пересечение всех результатов. Вопрос, зачем нам объединять не пересекающиеся результаты?
Так объясните, зачем мне оборачивать в какие-то объединенные типы, если можно написать прямо, либо то, либо то? И ограничивать себя невозможностью трех вариантов? С какой целью?
Ну это важный момент, я считаю побочные эффекты — результатом выполнения функции. Так что, считайте, что для меня не существует детерменированных, не чистых функций. Если дергаем что-то внешнее, то мы грязные. Ответ на ваш вопрос — я говорю и говорил о чистых функциях, как о чистых функциях.
Вы можете называть один из возможных результатов ошибкой, в go так и делают.
Теперь представьте, что есть доп. условие, что алгоритм для графов размером более N, должен сохранить его в файл и не возвращать ничего. Вы скажите, плохая композиция функций. Верни ошибку, оставь мне мою чистую функцию. А я скажу, ну да, значит с грязными придется возиться мне, а она плохая только в вашем языка, поддерживающем лишь описание функций с возвратом значений, мало того, с возвратом лишь одного значения. А если бы у вас были бы другие возможности, явного описания что способна сделать функция, какие внешние зависимости дернуть, а не только какие ей требуются, то и рассуждали бы мы по-другому.
lair
Ну то есть на каждый вызов функции без проверки результата (включая пример из поста Пайка) надо реагировать как на code smell?
Потому что множества ошибок и полезных данных не пересекаются.
Так я и пишу — либо то, либо то. А оборачивать затем, что над ними впоследствии работают монадические операции, позволяющие легкую композицию.
Да, кстати, а в Go разве можно написать либо то, либо то? Мне кажется, что возврат функции в Go — это k переменных, отношения между которыми языком никак не проверяются. Я не прав?
Нет никакого ограничения. disjoint union types — да, ниже правильно пишут, они же типы-суммы — могут собираться из произвольного количества типов. Более того, если надо, можно взять тип-произведение и получить ровно то же поведение, что в Go.
Ну и что? Декомпонуем алгоритм на две части, первая теперь возвращает вместо
Try[Option[Path]]
—GraphResult(Path(Vertex..) | NotFound | NegativeCycle | GraphTooLarge)
, а вторая вызывает первую, и в случае последнего результата делает сохранение.Простите, а как бы здесь помог возврат нескольких значений?
Пример приведите, если не сложно. Я не очень понимаю, о чем вы говорите.
arvitaly
Я уже написал, что нужно знать, что делает функция, а не пытаться проанализировать ее по названию, а в целом, мое мнение, что да, именно так, именно об этом я и сказал, что для разработчика go — это будет равносильно пустому catch. А ответом на вопрос насколько это эффективно, является ответ, насколько много в реальных задачах функций, возвращающих четко один тип значения (т.е. без возможности ошибок). По моему опыту, это меньше 1% функций, да и то с оговоркой, что panic-ошибка все равно возможна (память кончилась у процесса).
Вопрос был, с какой целью нам это бесполезное действо?
Там где мне нужны монадические операции, я будут использовать обертывание (и даже без встроенной поддержки монад), но это не ответ на вопрос, зачем это везде? Мне далеко не всегда нужно передавать этот супер-пупер тип куда-то дальше, чем место вызова.
Так с этим я не спорю, я утверждаю, что необходим механизм. позволяющий вернуть несколько различных типов значений без оборачивания в новый тип, так как в большом количестве случаев, это будет бессмысленное действие.
У вас появился тип GraphResult, который нужен, только при условии его пропихивания куда-то дальше, чем непосредственный вызов функции.
Возвращаем 3 варианта значений, либо путь, либо ошибку, либо запрос на запись в файл. Функция становится чистой, несмотря на то, что в ней зашит алгоритм записи в файл. Наша композиция перестает зависеть от возможностей языка.
lair
Это плохой (даже, наверное, очень плохой) подход. Он явно противоречит одному из основных принципов управления сложностью в разработке — сокрытию информации.
Ну то есть вот такой код:
Это code smell?
Оно не бесполезное. Мы сейчас к этому вернемся.
А вот теперь вспомним вашу же фразу:
Это означает, что 99% процентов функций (по вашим словам) возвращают либо ошибку, либо значение (набор значений). Это, в свою очередь, означает, что в 99% случаев вам нужна обработка ошибок, а ее, поверьте, в таких сценариях удобнее делать монадической композицией. Пример хотите, или на слово поверите?
Не бессмысленное. Тип-произведение (в сочетании с матчингом) позволяет сделать все то же, что и прямой возврат нескольких значений, и кое-что еще, чего такой возврат не позволяет.
Я, на всякий случай, еще раз спрошу: а как при возврате нескольких значений (как в Go) указать, что может быть либо одно, либо другое? Для ошибок это характерная ситуация.
Ну во-первых, он нужен, чтобы определить формальный контракт функции. Во-вторых — ну окей, это называется анонимные типы-суммы. В OCaml есть (похожие) полиморфные варианты, в Rust собираются запилить вот прямо анонимы как есть.
Серьезно?
Try[Either[Path | IO -> Try[unit]]]
. Ну илиTry[~[Path | None | IO -> Try[unit]]]
, если вспомнить, что пути может не быть, и взять анонимный тип.Зависит-зависит.
Во-первых, вам нужна возможность возврата нескольких взаимоисключающих значений.
Во-вторых, вам нужна возможность возврата «запросов» (я тут для этого использовал делегат, но это не единственный вариант, по идее).
arvitaly
Ок, для вас информация скрыта, когда вы не читаете документацию (и сигнатуру), а пытаетесь угадать, что же делает этот функция по названию. Искренне извиняюсь, но для такого принципа управления сложностью у меня есть название «бабка ванга».
Конечно, причем в любом языке.
Извините, но нет. Если A равно 1%, то это не значит, что B равно 99%. 99% в данном случае, будет 2 и более результатов, а не один результат и ошибка. 5 моих постов вам не хватило, чтобы понять, что кроме значения и ошибки могут быть еще результаты выполнения функции? Цитату привести, или на слово поверите?
Вот и используйте, там где не бессмысленное и где возврат не позволяет, но не для обработки множественных результатов функции.
Эм, любая сигнатура функции с 2 результатами? (int, int)? В чем вопрос? При вызове, проверяем оба или что хотим.
В языках без множественных результатов, конечно приходится контракт описывать костылями.
Суть не меняется.
Либо лыжи не едут, либо что. Вы все пытаетесь соорудить новый тип там, где он просто не нужен.
lair
Странно, что его тогда рекомендует тот же МакКоннел.
Тогда зачем Пайк приводит его как валидный?
Ну написано же: «либо ошибку, либо значение (набор значений).»
Почему? Как уже сказано, я ничего не теряю (потому что могу сделать все то же самое, что и с множественными результатами).
Эта сигнатура как-то запрещает вернуть одновременно валидные значения в оба результата (скажем, (5, 3))?
А как вы в языке с множественными результатами (только если можно, приведите конкретный пример на конкретном языке) опишете контракт функции вида «путь или путь не найден или найден отрицательный цикл» (все «или» — исключительные)?
Меняется. Нового типа не возникает.
Где вы видите новый тип?
arvitaly
Лично я теряю время на бессмысленное усложнение.
Опять вы вводите понятие валидность, что говорит о том, что вы всем нутром хотите 1 результат. Нет, сигнатура никак не запрещает вернуть 2 результата, ведь для этого она и создана.
Первые два результата будут отсутствовать. При вызове проверяем все.
Оборачивание в Try, Either, в юнион-типы, да во что угодно — бессмысленное синтаксическое усложнение кода. Как я уже написал — это новый тип возвращаемого значение, пусть будет безымянный тип, суть не меняется.
lair
Нет никакого усложнения.
Нет, это говорит о том, что у функции есть семантика. Большинство функций либо выдают (возвращают, бросают — не важно) ошибку, либо производят полезное действие (возвращают результат, не важно, одиночный или множественный, или произволят побочные эффекты). Соответственно, если мы получили от функции ошибку, нам не интересны возвращенные результаты (более того, в ряде случаев они опасны), поэтому если система типов позволяет явно сделать их недоступными — это уменьшает количество потенциальных ошибок.
Значит, она не позволяет описать семантику «либо-либо».
Я же просил: конкретный пример. Вот прямо кодом, с конкретными типами.
Это если синтаксис таков, что вы не можете с этим комфортно работать. А в норме вы всего этого просто не видите, потому что автовывод.
arvitaly
1. У функции есть семантика, но она не говорит нам о том, что полезное действие должно быть одно. Это вы пытаетесь, за неимением инструментов, объединить результат этих полезных действий, зачем?
2. Приемочные тесты должен выполнять не тот, кто разрабатывает, ТЗ должен писать заказчик. Если с этим вы согласны, то почему функция должна решать, что является результатом, что ошибкой, что предупреждением, а что побочным эффектом? Семантика функции лишь определяет ее возможности, но никак не оценку результата.
«Удар рукой» определяет что нужно сделать, а не считать ошибкой промах. А если удара не было (сердце остановилось), то это не ошибка, а не исполнение функции, и к ней никакого отношения не имеет уж точно (в go, panic error). «Удар рукой по сопернику» предполагает как перемещение руки (в итоге, она окажется в другом месте), так и соприкосновение, мало того, соперник может уклониться и соприкосновения не будет, но функция отработала верно, при этом, что из этого «результат», что «побочный эффект», а что «ошибка» никак не может описать семантика «удар рукой по сопернику».
Если вы поймете теорию выше, мы перейдем к практике, обещаю.
Автовывод никак не решает проблему обратного разделения union-type (или любой вашей другой оболочки) от разворачивания в момент использования, т.е. он тут не причем.
lair
SRP?
Потому что это заложено в ее контракт.
Отнюдь. Если в контракт функции заложено «я гарантирую, что будет возвращен корректный путь по графу, в противном случае вы не получите путь, а получите ошибку», то именно это и определяет оценку результата.
А что, просто так вы написать конкретный пример функции, выполняющей заданный контракт, вы не можете?
Какого обратного разделения? Какого разворачивания? Можете пояснить?
arvitaly
SRP и способ уведомления о результатах никак не связаны.
В go заложено да, а в языках с одним результатом — нет такой возможности, только «да или нет» (ну, в Java есть костыль в виде throws).
Вот оцените ваш русский язык, «я гарантирую, но и не гарантирую, а еще когда я не гарантирую, то не гарантирую по-разному (вот вам список исключений), а если вам захочется узнать подробностей, вот тут у меня есть друг (Logger), которому я все сообщу тайком, но на самом деле вы еще как-то мне его контакты сообщите. Кстати, не забудьте, что бывает еще пара случаев, когда я вроде бы гарантирую, но с оговоркой, а это я сообщу еще одному гостю программы (WarningGuest), правда не забудьте его сами же и позвать».
Извините, но такие гарантии мне напоминают Почту России и русский авось. А я лучше уж заложу возможность потери посылки, кражи, плохой логистики и заранее предупрежу об этом клиента. А он учтет каждый из этих вариантов в своем бизнес-процессе. А не будет сидеть и ждать ошибки Почты. Некоторые называют это профессионализмом.
Я вам написал (int, int), вы его не поняли, значит, нужно сначала разобраться с теорией.
Есть два варианта использования различных упаковок (типа, монад). Первый вариант, тот который и задумывался — семантическая инкапсуляция, когда мы, ради читабельности на естесственном языке, подразумеваем под одним словом различные реальные действия, т.е. полиморфизм.
Второй вариант — из-за невозможности вернуть 2 результата одновременно (а желание возвращать один результат исходит из неправильного понимания слова «функция»), мы объединяем результаты не ради семантики, а потому что технически невозможно по другому. Чаще всего при этом рождается монстр-переменная или функция, состоящая из 2 слов Результат1ИлиРезультат2 или еще более непрозрачные термины, типа GraphResult. Единственная цель второго варианта в 99% случаев, тут же распаковать (выполнить) наш монадический контейнер и достать Результат1 и/или Результат2.
lair
Зато SRP и количество «полезных действий» в функции связаны напрямую.
Это банальная неправда (я уж не знаю, от незнания или намеренно). Примеров выше по треду достаточно.
(про Go, в принципе, тоже можно поспорить, но не буду)
Это не мой русский язык. С пугалами сражайтесь без меня.
Пожалуйста, приведите пример такой реализации.
Этот пример не имеет отношения к поставленной задаче. Напомню ее еще раз:
Я вас расстрою, но «распаковка», как вы выражаетесь, контейнера — а иначе говоря, операция над результатом — будет выполняться в 99% процентах в обоих сценариях. Поэтому я не вижу, как вы разделите первый и второй.
А так — я искренне вас прошу перестать регулярно подменять типы-суммы (результат1 или результат2) типами-произведениями (результат1 и результат2). Возврат двух (или более) результатов из функции одновременно — это только второй сценарий. Первый вы при этом успешно игнорируете.
lair
Да, по поводу «невозможности вернуть два результата одновременно».
Скажите, вот это — два результата одновременно?
vintage
Справедливости ради стоит заметить, что «ошибки»и «результат» не всегда взаимоисключающи. Ошибки, которые могут сосуществовать с результатом называются обычно «предупреждениями». Exception и Option тут сосут.
lair
(Вы уж определитесь, ошибки или предупреждения.)
Но за вычетом терминологии, это-то как раз тривиально:
let (res, warnings) = giveMeWarnings()
vintage
Опять же, «предупреждения» и «ошибки» тем более друг друга не исключают. И, кстати, ошибки тоже могут быть во множественном числе (и очень удобно, когда язык это таки поддерживает, а не вынуждает костылять исключение «список исключений»).
Выглядит как костыль. Опять же, что считать ошибкой, а что предупреждением, решать должен вызывающий код, а у вас получается это решает вызываемый.
Лучшее решение выглядит так:
let res = getConfig()
when FileNotFound res = makeDefaultConfig()
when error is ValueIsEmpty return log( error ).ValueType.default
lair
Смотрите, для меня семантическое разделение очень просто: когда вызываемый метод завершил свою работу «нормально» (с его точки зрения), то он может вернуть только предупреждения. Когда он завершил свою работу «ненормально» (результаты надо игнорировать) — он возвращает ошибку (можно с предупреждением). То есть мнение вызываемого о предупреждении или ошибке — это можно доверять его результатам или нет.
А дальше уже вызывающий сам решит, падать ему, или откатываться.
Что-то мне это напоминает лисповские кондишны.
vintage
В том-то и дело, что вызываемый не обладает всей полнотой информации, чтобы принять решение ошибка это или предупреждение. Его задача — зафиксировать, что что-то пошло не так, как ожидалось, а задача вызывающего кода принять решения что делать (подставить дефолтное здачение, раскрутить стек, проигнорировать). И да, это лисповые кондишены и есть.
lair
Я не уверен, что это решение хорошо для всех случаев — теперь вызывающий слишком много знает о внутренностях вызываемого. Но да, кондишны — штука очень мощная.
vintage
Не, вызывающий ничего не знает о внутренностях, он лишь предоставляет стратегии для разных кейсов. А уж как ими распорядиться решает вызываемый код.
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
lair
Для того, чтобы предоставлять стратегии, надо знать список кейсов. Иногда это оправдано, иногда — избыточно.
vintage
Статического анализа вызываемых методов вполне достаточно, чтобы вывести пользователю список возможных кейсов. При этом не надо копипастить портянки возможных кейсов в сигнатуру каждой функции.
lair
Если это будут только наименования, то этой информации не всегда достаточно, все равно надо знать условия возникновения.
Я не говорю, что этот подход совсем невозможен, я просто говорю, что у него есть свои недостатки, первый из которых — уменьшение инкапсуляции.
vintage
Колбэк, реализующий фиксированный протокол никак не нарушает инкапсуляцию. Вызывающий код имеет доступ лишь к той информации, которую вызываемый ему предоставил. Собственно и монады и исключения «нарушают инкапсуляцию» ровно в той же степени, необходимой для принятия решения.
lair
Вот вопрос как раз в количестве раскрываемой информации. Я не знаю, где баланс между «слишком мало, ничего не могу сделать, ем, что дают» и «могу сделать что угодно, слишком много информации, мозг взорвался».
vintage
Чем больше, тем лучше. Важно лишь, чтобы можно было поменять внутреннюю реализацию, оставив внешние интерфейсы неизменными.
lair
Вот это и противоречит принципу сокрытия информации.
vintage
Нет такого принципа. Есть принцип инкапсуляция сложности — она к сокрытию информации никакого отношения не имеет.
lair
«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)»
vintage
Ну, в качестве антипаттерна такой принцип есть, да :-)
Скрытие информации — один из способов реализации абстракций, который создаёт лишь дополнительные проблемы, когда требуются разные уровни абстракций. Яркие примеры, когда нужен низкий уровень абстракций — тесты, обобщённая сериализация. При этом для реализации абстракций вовсе не нужно скрывать информацию. Яркий пример — питон, где все поля объекта публичные и соответственно нет проблем с рефлексией. А яркий антипример — яваскрипт, в котором некоторые умники рьяно пытаются применить сокрытие информации, делая свои модули не просто нерасширяемыми, но и чертовски сложно отлаживаемыми.
lair
Неа. Скрытие информации — это один из способов борьбы со сложностью, который, в частности, связан с абстракцией.
Например, какие?
Тесты работают с реализацией, там абстракции (тестируемого класса) вообще не очень интересны. Обобщенная сериализация просто работает с другим интерфейсом сериализуемого объекта, нежели основные пользователи.
Учитывая вышесказанное, я предпочту поверить в этом вопросе своему опыту (ну и заодно — МакКоннелу, Парнасу, Бруксу, исследованиям Боэма и Корсона-Ваишнави), нежели вам.
vintage
Сокрытие информации ни коим образом не поможет вам в борьбе со сложностью (какие соглашения нужно знать, чтобы с этим работать). Со сложностью борятся именно абстракции, которые позволяют вам не думать о деталях реализации, когда вам этого не нужно. При этом скрывая информацию вы создаёте себе сложности, когда вам нужно знать детали реализации.
Вот именно, что тесты работают с реализацией и для полноценного её тестирования требуется доступ к «приватным» членам. Типичный способ обойти сокрытие информации — запихивание тестовых сценариев напрямую в тестируемый класс в качестве методов.
Обобщённая сериализация использует более низкоуровневые абстракции, раскрывающие всё внутреннее состояние, вплоть до тупого, но наиболее эффективного дампа памяти.
Ну вот, свели аргументированную дискуссию в плоскость веры :-D
lair
Почему не поможет? То, чего я не знаю, не отнимает моего времени на обработку.
А что мне мешает пойти и посмотреть детали реализации? Другое дело, что необходимость их знать обычно означает, что что-то не так с дизайном (проще говоря, абстракции потекли).
Зачем? Тестируйте по публичному контракту.
Серьезно типичный? На мой взгляд, он отвратителен. Есть (как минимум в C#) много других замечательных способов это делать.
Ну и как этому мешает сокрытие информации?
vintage
А как поможет? Доступность информации тоже не отнимает время на обработку.
Речь о программном доступе. Абстракции всегда текут.
Как протестировать инкапсулированный в объекте кэш исключительно через публичный интерфейс?
interface IURI { string getParam( string name ) }
Отвратителен, но работает. А что за замечательный способ есть в C#?
Как раскрытию информации мешает её сокрытие? Ну даже не знаю :-D
lair
Отнимает. Любая избыточная информация требует ресурсов в голове.
А вот если вы полезли туда программно, значит, с дизайном все совсем плохо.
Слишком мало информации для ответа на вопрос. Но публичный интерфейс объекта, в любом случае, это не интерфейс в том смысле, который вы выдали.
Самый простой — рефлексия. Еще есть наследование, internal и так далее.
Вы не понимаете. Сокрытие информации — это в первую очередь от людей. А ваши низкоуровневые абстракции — они не для людей, и не для пользовательского кода.
vintage
Существование википедии наверно вообще мозг взрывает?
Если полез туда программно, значит в том есть необходимость. Хорошо, наверно жить в мире непротекающих абстракций?
URI инициируется строкой, при запросе getParam он лениво парсится и полученная внутренняя структура помещается в кэш. Таким образом последующие вызовы getParam происходят гораздо быстрее. Как вы реализуете этот класс и как протестируете, что повторный вызов getParam минует парсинг?
Ну да, рефлексия, паблик морозов и другие костыли для обхода проблемы, созданной безудержной манией сокрытия данных.
Они для тех людей, которым они нужны. А кому не нужны — могут ими не пользоваться.
lair
Существование — нет. Необходимость ее знать — да.
Вынесу парсер в отдельный юнит, замокаю его, и удостоверюсь, что к нему нет лишних вызовов.
Эта «мания» может уменьшить расходы на модификацию программы в четыре раза.
Я в своем опыте на редкость мало видел проблем, связанных с избыточным сокрытием. Плохой нерасширяемый дизайн — видел. А избыточного сокрытия — практически нет.
Ну вот я и не хочу, чтобы они были в том интерфейсе, которым я пользуюсь.
vintage
Так вас никто и не принуждает всё знать.
Ну вот, вместо одного простого юнита мы имеем уже 3, да ещё и с DI небось, чтоб уж совсем «хороший дизайн». При этом парсер попадает ещё и в публичный интерфейс, то есть, ради тестов мы раскрываем информации больше, чем необходимо пользователю. Сокрытие данных в этом случае приводит к неоправданному увеличению сложности, давая взамен лишь эфемерное «может уменьшить расходы на модификацию программы в четыре раза». Каким образом наличие доступа к информации усложняет рефакторинг видимо так и останется без обоснования.
Возможно потому, что в C# всегда можно обойти сокрытие, через субклассинг, рефлексию и прочее. А вот в том же JS, сокрытие фиг обойдёшь.
Ну, я тоже хочу чтобы все инструменты были заточены исключительно под мои текущие задачи и не умели ничего, кроме этого. Только вот задачи меняются, а допиливать инструменты специально под меня никто не будет.
lair
Вот когда не принуждает — это и есть information hiding.
Откуда три? Либо два вместо одного, либо три вместо двух.
В тот публичный интерфейс, который виден пользователю не попадает.
Это не сокрытие данных, это дизайн под тесты. Расскажите, как бы вы это реализовывали, если бы сокрытие данных вам не «мешало»?
Оно не эфемерное. Korson, Timothy D., and Vijay K. Vaishnavi. 1986. “An Empirical Study of Modularity on Program Modifiability.”
Очевидно же — если информация не используется, она просто усложняет работу мозга. Но скорее всего избыточное знание о компоненте будет использоваться, а это приведет к более сильной связности, а та, в свою очередь, усложняет рефакторинг.
Необходимость использовать рефлекцию для обхода information hiding (за небольшим исключением инфраструктурных вещей) — это уже проблема. Обычно вытекающая из плохого дизайна.
Значит, в нем больше требований к дизайну.
А как же милый сердцу любого программиста мир опен-соурса?
Но если серьезно, то хороший дизайн подразумевает изменяемость без избыточного раскрытия информации.
vintage
А когда вас не заставляют идти в школу — это тюремное заключение, ага.
URI, URIParser, MockURIParser. Наверняка ещё и URICacher захочется вынести. Ах да, ещё и URISerializer. Сделаем звездолёт ради священного «хорошего дизайна»!
То есть вы предлагаете ещё и URIFacade сверху прилепить, чтобы скрыть получившуюся «правильно задизайненную» сложность?
Я просто проверю 3 кейса: parseURI формирует правильную структуру; getParam берёт данные из этой структуры; если структура не создана, то getParam её создаёт.
Там сравнивается монолитный и модульный дизайн. При чём тут сокрытие информации?
Если оно используется, значит оно не избыточное. Например, в питоне все поля публично доступны, но благодаря соглашению об именовании все знают, что полям начинающимся с подчёркивания стоит предпочитать поля начинающиеся с буквы. Но при этом никто не вставляет в палки в колёса и если программисту нужно вывести в лог красивый дамп объекта — это делается элементарно, без необходимости в каждом объекте реализовывать интерфейс IPrettyDebugString.
Ну да, всё, что не вписывается в догму сокрытия данных просто объявляется плохим дизайном :-) Главное — никогда не сомневаться в своих убеждениях.
К сожалению, большинство программистов считают хорошим дизайном совершенно не то, что вы или я. Например, jQuery, обладающий ужаснейшим дизайном и реализацией, завоевал мир. И завоевал главным образом благодаря тому, что прячет сложность за простым интерфейсом — именно это нужно обычным программистам, а не информационные шоры.
lair
Омг, но зачем? Делаем
IUriParser
, делаем реализацию, которая всегда парсит, затем делаем кэш (с таким же интерфейсом), в который и заворачиваем. А моки никто не считает, их реализовывать уже не надо.Угу, теперь ваш тест жестко привязан ко внутренней реализации (структуре и parseUri), хотя для пользовательского поведения важна только getParam. Собственно, в этот момент вы заменили тест по поведению на тест по реализации, а второй всегда сложнее в поддержке.
Вы уже прочитали всю работу?
Вот при этом: www.ifsq.org/finding-dp-5.html
Далеко не факт. Ну и связность повышается, как уже сказано.
Угу. Теперь помимо обычного ООП мне надо помнить это «соглашение», а заодно у объекта нет никаких способов поддерживать инкапсуляцию.
Догма сокрытия данных тут ни при чем. Необходимость использовать reflection (за исключением специфических задач) — это плохой дизайн.
Эээ, а ничего. что «прятать сложность за простым интерфейсом» — это и есть (в числе прочего) information hiding?
vintage
1. Разделять IURIParser и IURISerializer — глупо, ибо они неразрывно связаны. Поэтому они должны быть частью одной абстракции — IURI.
2. Формат структуры (привет, пародия на ооп в виде data-object) у вас внезапно становится частью публичного интерфейса.
3. Парсинг опять же может происходить в несколько ленивых шагов — сначала парсим URI, потом парсим queryString. Прикрутим IQueryStringParser? В моём случае вся эта сложность инкапсулируется в одной абстракции — IURI.
4. В вашем случае моки всё же придётся реализовать ввиду специфичного поведения.
Для внешнего кода не менее важно, что он может спокойно обращаться к getParam и быть уверенным, что это не приведёт к просадке производительности. Так что да, мои тесты проверяют, конкретную особенность реализации, которую иначе не проверить, без раскрытия информации. И предлагаемая вами декомпозиция — не более чем способ раскрыть информацию, лишь формально не нарушая принципа сокрытия данных, вплоть до полного отсутствия состояния в объектах, оставляя лишь функции и гоняя между ними структуры.
Если вы хотите что-то возразить — приводите аргументы или хотя бы цитаты чужих аргументов. Не заставляйте меня искать ваши аргументы за вас :-)
Я вам открою страшную тайну, но для программирования нужно знать куда больше абстракций, чем только лишь ООП, и соглашение об именовании *в рамках языка* — сущий пустяк.
Весь этот тред и вырос из моего тезиса, что инкапсуляция сложности и скрытие информации — разные вещи, а не синонимы, как многие считают. Чтобы уменьшить сложность достаточно предоставить простой интерфейс, но для этого вовсе не обязательно ограничивать доступ. Более того ограничение доступа меняет одну сложность на другую.
Пока что вы не привели ни одного аргумента за сокрытие информации, кроме:
1. Как мне кажется это хороший дизайн
2. Ссылки на сомнительное исследование
lair
Все это решается кэширующим декоратором вокруг
IURI
.Какое же в них такой специфичное поведение, которое нельзя покрыть Moq?
… а надо проверять то, что ожидает внешний код — отсутствие просадки по производительности.
Да я, собственно, уже привел. Вам цитату целиком надо дать? (похоже, МакКоннела вы не читали) Да пожалуйста:
Вот из таких ненужных пустяков и складывается мусор, из-за которого на собственно бизнес-то места в голове и не остается.
Я нигде и не говорил, что это синонимы. Сокрытие информации — это один из инструментов управления сложностью.
Leeb
По-моему, lair под union type имел в виду не то, что вы подумали, а тип-сумму (в том смысле, в котором это понимает теория типов), или алгебраический тип данных. На Haskell это будет выглядеть примерно так
Эта запись как раз и значит, что значение этого типа — это или одно (Just a), или другое (Nothing, ничего). Option[A] в Scala работает ровно так же. И никаких toString и прочей ерунды. Если нас интересует не просто отсутствие значения, а какая-то индикация в случае, когда значения нет (ошибка), существует тип Either, который можно параметризовать типами успешного и неуспешного результата (хотя в данном случае они абсолютно равнозначны, есть просто соглашение).
Большое преимущество такого подхода в том, что за правильностью обращения с этими типами следит тайпчекер, а не какой-то линтер, предупреждения которого, в общем-то, можно и проигнорировать. Он же и следит за тем, что мы рассмотрим в конечном итоге все варианты, которые могла вернуть функция (и Just, и Nothing). Никакой динамической типизации это и не снилось.
Заметьте, функция не начинает возвращать несколько значений (если вам так хочется, вы, конечно, можете воспользоваться типом-произведением, иначе кортежем, но технически это будет всё равно одно значение). Не надо изобретать велосипед, эти вещи вполне себе известны давным давно и успешно применяются. Другое дело, что это всё тянет за собой параметрический полиморфизм, сопоставление с образцом, часто нормальную композицию функций, и вообще размышления о компонуемости примитивов в языке. Конечно, проще вернуть два значения (а если захочется 3? какое из них будет ошибкой?), и объявить всё остальное не нужным.
И раз уж вы так уверены, что возврат ошибки превращает чистую функцию в нечистую, расскажите мне, нужна ли коммутация с внешним миром парсеру? Ведь для одного и того же входа с синтаксический ошибкой он будет возвращать всегда одну и ту же ошибку, а для одного и того же верного входа — всегда одно и то же AST?
arvitaly
Я подумал про тип-сумму, только сказал, что создание такого типа — костыль, бессмысленное действие, необходимое лишь для того, чтобы в языках с одним результатом выполнения функции вернуть два.
Я в общем-то не сильно настаивал на динамической типизации, просто сказал, что для меня проще совсем без типов (вернее, перенос их в тесты), чем использование таких костылей.
Спасибо за совет, но мне не нравится пользоваться костылями, даже давно и успешно применяющимися.
Никакое, потому что и нет понятия ошибки на уровне языка. А в документации конкретной функции написано какое.
Нечистой она становится только при условии, что у нас нет возможности указать и обрабатывать несколько результатов выполнения (ну мы сейчас не говорим о побочных эффектах).
С Union-type функция будет чистой, мой тезис — union-type не нужен.
Не понял вопроса, что значит нужна ли? В языках, где в возврат значения невозможно включить вызов внешнего мира, лучше декомпоновать, как предложил lair.
Leeb
Стоит начать с того, что АТД были созданы не только для возврата значений. Я даже думаю, что об этом вообще не думали, эта возможность получилась вполне естественно вытекающим из системы типов образом. В отличие, кстати, от возможности возврата нескольких значений, которую в любом случае надо было явно заложить в язык.
Более того, АТД легко и просто позволяют кодировать 2 взаимоисключающих значения (именно поэтому они disjoint), то, что нужно, когда нам нужно вернуть значение или причину, по которой его вычисление не удалось. Вы же предлагаете всегда возвращать N значений, которые, чаще всего, взаимоисключающие, помечая отсутствие специальным значением. Согласитесь, что отсутствие значения (когда мы возвращаем одно значение, другого у нас просто нет, оно не null, не nil; оно отсутствует и взять его не откуда) и специальное значение, которое по договорённости означает отсутствие этого самого значения — вещи несколько различающиеся (не говоря уже о том, что второе вообще звучит, как бред).
Я, кажется, каким-то отличным от вас образом понимаю значение слова «костыль».
arvitaly
То-то и оно.
Не естественным, а просто подогнали, как в Common Lisp якобы гладко легло ООП.
Да, я уже написал, что в go это тоже сделано не идеально, однако гораздо лучше, чем исключения или динамическая типизация, или union-типы.
Предложите вариант, который мне понравится, с удовольствием вас поддержу.
maledog
Выбросить пользователю исключение в 50 Mb или послать его по почте программисту?
Сорри. не в ту веточку.