Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
ew.write(getAB()) возникает ошибка, то почему выполняются getCD() и getEF(), когда в их результирующих значениях смысла нет?»Это не отвечает на вопрос «если в строке ew.write(getAB()) возникает ошибка, то почему выполняются getCD() и getEF(), когда в их результирующих значениях смысла нет?»
изначально маленькая команда разработки Go сознательно отказалась от этой концепции учитывая предыдущий опыт.
Насколько я знаю концепция исключений для управления состоянием, в общем случае, считается антипаттерном.
К этому, в частности, привело то, что во многих ЯП, отношение к исключениям, сама логика их работы и реализация менялась со временем. И как я понимаю, изначально маленькая команда разработки Go сознательно отказалась от этой концепции учитывая
Реализация большинства монад хоть и проще стека исключений, все равно требует достаточного времени на тестирование
Под состоянием я имел ввиду логику исполнения (control flow).
goto), но нет ничего плохого в использовании исключений для сигнализации о возникновении ненормативного сценария выполнения (и переключения потока выполнения в этот сценарий).[в C#] считалось, нерациональным использовать исключения для управления логикой из-за огромных накладных ресурсов и просадки производительности.
Собственно говоря, мой вопрос о чем-то посредине остался без ответа
Тут ниже уже написали по макрос для 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)
}
Действительно, исключения в .net ресурсоемки — но это не значит, что невозможно сделать нересурсоемкую реализацию исключений.
Хорошо, а как тогда запретить использовать исключения во всех остальных случаях?
обственно, насколько я знаю, причины отсутствия исключений в Go, это сложность реализации и желание ограничить область использования исключительно ошибками.
По вашим же ответам почему-то создается ощущение, что это если не пустячное дело, то говорить тут особо не о чем.
Более того, вот вы пишите, что до сих пор это считается порочной практикой в C#, а на деле это происходит достаточно часто.
Насчет монад, уже вроде решили, что сделать на Go так нельзя.
исключения в .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);
}
}
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
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
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
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
выброс исключения ситуация исключительная, программа в продакшене должна работать без них
Класс Stopwatch основан на HPET (High Precision Event Timer, таймер событий высокой точности). Данный таймер был введён фирмой Microsoft, чтобы раз и навсегда поставить точку в проблемах измерения времени. Частота этого таймера (минимум 10 МГц) не меняется во время работы системы.
А во всех случаях ответ один и тот же — потому что код написан неэффективно. Так зачем же предлагать заведомо неэффективное решение?
Но тем не менее, ответа на вопрос «что делать с кучей повторяющихся `if err != nil { return err }`.
что код ошибки должен быть вторым значением
сделайте то же, чтобы вы делали с «кучей повторяющихся» кодаИз поста в пост вы твердите одно и тоже. Но…
Однако это, епрст, не отменяет моего другого ощущения: «wtf???»
Fatal error: Uncaught exception 'Exception' with message
Код в статье был примером, служащим для демонстрации подхода.
И этот пример как раз демонстрирует неэффективность.
Раз уж вы так хотите использовать в повторяющихся вызовах write() какие-то функции в качестве параметров, но не хотите, чтобы они вызывались в случае ошибки — сделайте так, чтобы они в этом случае не вызывались! Это же просто. Функции это такие же значения, и во write можно передавать саму функцию, а не ее результат, и запускать функцию после проверки на err.
write меняется с «обычного» значения на функцию? Или добавляется новый метод с другим типом входного значения?write, но вот сам (избыточный) вызов write никуда не делся. Надеюсь, его накладная стоимость не очень велика.Кроме того, если вы уже настолько синтетически придумали пример
_, 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
}
try...catch?У меня только один вопрос: а чем это лучше try...catch?А кто сказал, что эта дикая смесь ошибок и исключений лучше try/catch? Она не лучше, она намного, намного хуже.
panic для решения проблемы, описанной в посте. Я неправильно понял совет? Как надо было?Но, кажется, я ненастоящий гофер: сымитируем панику — и вывод становится неожиданным:
Success
Если в методе есть тяжелые вычисления, то это… обидно.
if-ов нравятся не больше.errn<Tab> чтобы вставить в код этот if на 3 строчки.В данном случае это не так — такой код читается очень легко, конструкции типовые, есть всего 2-4 варианта которые обычно используются, и которые со второго-третьего дня работы с Go начинаешь моментально распознавать.
конструкции типовые, есть всего 2-4 варианта которые обычно используются, и которые со второго-третьего дня работы с 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, что Вы постоянно критикуете его в комментариях, может быть Вам стоит потратить несколько недель и попробовать его в реальном проекте?
Нет, несомненно, «ошибки — это значения», и поэтому к ним применимы все языковые средства во всем их разнообразии… вот только униформность в этом случае сильно страдает, а вместе с ней начинают рассыпаться и приведенные вами аргументы.
Извините, я, наверное, сильно устал, но я не уловил логики в этом утверждении. Можете развернуть мысль подробнее?
Покажите мне, пожалуйста, конкретные примеры кода стандартной библиотеки, о которых Вы говорите
func (b *Reader) ReadString(delim byte) (line string, err error)
line, err := reader.ReadString('\n')
if err != nil {
// process the error
}
func (s *Scanner) Scan() bool
//***
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
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()
}
те ошибки, которые Go не считает «исключительными», на самом деле, почти всегда cебе очень даже и исключительныеИсключительная ошибка или нет знает только конечное приложение. Библиотека этого знать не может в принципе. Поэтому использование panic в приложении вполне уместно, а в библиотеке крайне нежелательно. Кроме того, логика обработки ошибок — важнейшая часть логики приложения, поэтому крайне желательно побуждать программиста над ней задумываться, а не лепить механически panic везде просто потому, что ему лень думать.
panic(err)). В абсолютном большинстве задач, с которыми работал я — текстового описания ошибки было абсолютно достаточно. и оно было намного удобнее сложной структуры. Желание всё усложнять без необходимости обычно проходит примерно через 10-15 лет работы программистом.Ну смотри, ты не прав.Великолепно! Кратко, и по сути!
Я считаю, что не стоит разделять на конечные пкг и приложения, не стоит этим заниматься.Почему, собственно?
конечному приложению нужны стектрейсы, если что-то в библиотеке сломалось.Насколько сильно сломалось? Если очень сильно — и так будет паника. Если библиотека просто некорректно работает и вернула не то значение или ошибку — в её коде всё-равно придётся разбираться, отлаживать и фиксить, и поиск места где эту ошибку вернули (на которое бы указал стектрейс, и то только в случае возврата ошибки-исключения, а если библиотека просто возвращает некорректное значение то никакого стектрейса бы не было всё-равно) обычно занимает секунды/пару минут, которые полностью теряются на фоне времени необходимого на отладку и исправление кода (да и это время не является бесполезно потраченным — чтобы исправить ошибку всё-равно нужно разобраться почему она возникает, т.е. вычитать этот же самый код).
Скажем так, я не вижу причины не использовать сквозные паники.Ну что тут скажешь… беда, просто беда. А я вот не вижу причины себя ими ограничивать, предпочитаю использовать тот подход, который лучше подходит в конкретной ситуации.
Откуда вывод — необходимо придумать что-то еще, совмещающее достоинства обоих методов и лишенное недостатков.
if..else ей не обязательны).Хорошо бы в заголовке каждой функции обязать прописывать исключения, которые она в принципе может генерировать.
fn write_smth(fd: &mut File) -> Result<()> {
try!(fd.write(b"blablabla"));
try!(fd.write(b"blabla"));
try!(fd.write(b"bla"));
Ok(())
}
$.ajax({
onOk200:()=>{},
onServerNotOkAnswer: ()=>{},
onNotConnect: ()=>{}
onCreated201: ()=>{}
});
в go нельзя обработать результат выполнения функции непонятно где.
Не вижу примера, где можно не получить все результаты функции при ее вызове.
UPDATE в SQL. Или вот еще более милое:func (tx *Tx) Commit() error
Под результатами функции я понимаю, именно множественные результаты одного вызова функции.
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
Тогда и с асинхронностью (читай, многопоточностью) проблем не будет.
Если я выполняю UPDATE, и мне не важно, сколько строк обновлено, я могу просто проигнорировать весь возврат.
fun funny i =
(i/2.0, i/3.0)
Программирование на continuations? Программирование на акторах? Имя им легион давно.
Покажите, как вы его проигноируете, вызовите, пожалуйста, функцию?
tx.Exec("UPDATE Users SET Active = 0")
во всех других сферах функция обязательно имеет зависимости (внешние системы)
Отсюда return null, throw new Exception и т.д. Хотя действительным результатом выполнения функции будет куча вызов внешней системы.
null или бросали исключения, при этом не имея никакой внешней зависимости. Даже у стандартных алгоритмов бывают недопустимые ситуации.tx.Exec(«UPDATE Users SET Active = 0»)
Вы это серьезно? А вы не пробовали декомпоновать вашу задачу таким образом, чтобы весь необходимый ввод от «внешних систем» получался отдельно, а потом приходил в вашу функцию входным значением?
Знаете, в моей жизни было много функций, которые возвращали null или бросали исключения, при этом не имея никакой внешней зависимости. Даже у стандартных алгоритмов бывают недопустимые ситуации.
Ну для разработчика go этот код равносилен пустому catch.
Однако, сложность как раз вызвать все эти зависимости и собрать всевозможные результаты.
Нужны механизмы для описания функции с множественными результатами с разными типами (FileNotFoundException, «file content», null).
Try[Option[T]].То есть разработчик Go наизусть помнит, какие функции возвращает ошибку, а какие — нет, и где такой вызов эквивалентен пустому catch, а где — нет?
Никакой особой сложности, по большому счету. Собственно, это экстремальное применение DI.
Вы так говорите, как будто union type — это что-то совершенно уникальное и недостижимое. А то, что вы сейчас описываете — это типичный Try[Option[T]].
Претензия не по существу. Смотреть документацию (или сигнатуру) функции перед ее использованием признак профессионализма,
Особой сложности вообще нет, есть просто сложность, включающая число сочетаний результатов вызовов зависимостей (будь то Exception или return).
Union type не интересен, повышает сложность и чтения и рефакторинга в разы, лучше уж сразу слабую типизацию.
Try — костыль, желание оставить один результат выполнения функции, зачем?
Это обман разработчика мнимой чистотой функции.
Код существенно чаще читается, чем пишется. Когда я читаю (бегло) код, я не хочу лезть смотреть сигнатуру каждой используемой функции. Я могу на глаз определить, проглочена ошибка, или нет?
Каким именно образом он повышает сложность? Можете на примере показать?
Чтобы явно указать, что функция может возвращать значение с семантикой «ошибка».
Почему мнимой? Как вообще тип возвращаемого значения связан с чистотой?
А вы можете на глаз определить, выкидывает функция Exception или возвращает null?
Ну все просто, как я уже сказал, функция может иметь действительно разные результаты (например строку или объект ошибки), и union type будет таким, что пересечение будет минимальным (toString?)
А зачем нам постоянно описывать этот тип, если можем просто указать, что может вернуться либо то, либо то?
Either[string,int]Для меня это означает, что функция может возвращать 2 разных значения, а «семантика ошибка» — игра слов, скрывающая это.
Ну чистая функция для одного входного значения всегда вернет то же самое выходное.
А здесь она еще оказывается может вернуть ошибку
Try[Option[Path]].В языках с исключениями полезно считать, что любая функция может их кинуть (это, в принципе, так). С null сложнее, но там можно приручить статический анализатор.
Извините, но union type — это объединение всех результатов.
Вы не понимаете. В общем случае, Try — это частный случай Either, в котором один из типов — это подтип Error. Поэтому Try всегда говорит, что может вернуться или ошибка, или осмысленное значение.
Кстати, а мы тут точно не путаем чистые функции с детерминированными? Для дискуссии это не очень важно, но любопытно стало. Я считал, что чистая функция — это функция без побочных эффектов.
Ошибка может быть реакцией на конкретный подвид входного значения.
Значение строго детерминировано входными данными, но при этом прекрасно покрывается семантикой Try[Option[Path]].
В языках без исключений полезно считать, что функция может вернуть ошибку.
Извините, для использования, это пересечение всех результатов. Вопрос, зачем нам объединять не пересекающиеся результаты?
Так объясните, зачем мне оборачивать в какие-то объединенные типы, если можно написать прямо, либо то, либо то?
И ограничивать себя невозможностью трех вариантов?
Теперь представьте, что есть доп. условие, что алгоритм для графов размером более N, должен сохранить его в файл и не возвращать ничего.
Try[Option[Path]] — GraphResult(Path(Vertex..) | NotFound | NegativeCycle | GraphTooLarge), а вторая вызывает первую, и в случае последнего результата делает сохранение.А я скажу, ну да, значит с грязными придется возиться мне, а она плохая только в вашем языка, поддерживающем лишь описание функций с возвратом значений, мало того, с возвратом лишь одного значения.
А если бы у вас были бы другие возможности, явного описания что способна сделать функция, какие внешние зависимости дернуть, а не только какие ей требуются, то и рассуждали бы мы по-другому.
Ну то есть на каждый вызов функции без проверки результата (включая пример из поста Пайка) надо реагировать как на code smell?
Потому что множества ошибок и полезных данных не пересекаются.
Так я и пишу — либо то, либо то. А оборачивать затем, что над ними впоследствии работают монадические операции, позволяющие легкую композицию.
Нет никакого ограничения. disjoint union types — да, ниже правильно пишут, они же типы-суммы — могут собираться из произвольного количества типов. Более того, если надо, можно взять тип-произведение и получить ровно то же поведение, что в Go.
Ну и что? Декомпонуем алгоритм на две части, первая теперь возвращает вместо Try[Option[Path]] — GraphResult(Path(Vertex..) | NotFound | NegativeCycle | GraphTooLarge), а вторая вызывает первую, и в случае последнего результата делает сохранение.
Пример приведите, если не сложно. Я не очень понимаю, о чем вы говорите.
Я уже написал, что нужно знать, что делает функция, а не пытаться проанализировать ее по названию
в целом, мое мнение, что да, именно так, именно об этом я и сказал, что для разработчика go — это будет равносильно пустому catch.
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// простыня
if err != nil {
return err
}
Вопрос был, с какой целью нам это бесполезное действо?
зачем это везде? Мне далеко не всегда нужно передавать этот супер-пупер тип куда-то дальше, чем место вызова.
А ответом на вопрос насколько это эффективно, является ответ, насколько много в реальных задачах функций, возвращающих четко один тип значения (т.е. без возможности ошибок). По моему опыту, это меньше 1% функций,
Так с этим я не спорю, я утверждаю, что необходим механизм. позволяющий вернуть несколько различных типов значений без оборачивания в новый тип, так как в большом количестве случаев, это будет бессмысленное действие.
У вас появился тип GraphResult, который нужен, только при условии его пропихивания куда-то дальше, чем непосредственный вызов функции.
Возвращаем 3 варианта значений, либо путь, либо ошибку, либо запрос на запись в файл.
Try[Either[Path | IO -> Try[unit]]]. Ну или Try[~[Path | None | IO -> Try[unit]]], если вспомнить, что пути может не быть, и взять анонимный тип.Наша композиция перестает зависеть от возможностей языка.
Это плохой (даже, наверное, очень плохой) подход. Он явно противоречит одному из основных принципов управления сложностью в разработке — сокрытию информации.
Это code smell?
Это означает, что 99% процентов функций (по вашим словам) возвращают либо ошибку, либо значение (набор значений).
Не бессмысленное. Тип-произведение (в сочетании с матчингом) позволяет сделать все то же, что и прямой возврат нескольких значений, и кое-что еще, чего такой возврат не позволяет.
Я, на всякий случай, еще раз спрошу: а как при возврате нескольких значений (как в Go) указать, что может быть либо одно, либо другое? Для ошибок это характерная ситуация.
Ну во-первых, он нужен, чтобы определить формальный контракт функции.
Во-вторых — ну окей, это называется анонимные типы-суммы. В 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))?
А как вы в языке с множественными результатами (только если можно, приведите конкретный пример на конкретном языке) опишете контракт функции вида «путь или путь не найден или найден отрицательный цикл» (все «или» — исключительные)?
Где вы видите новый тип?
Лично я теряю время на бессмысленное усложнение.
fun goLike a = (a*2, null) let (b, err) = goLike 3
Опять вы вводите понятие валидность, что говорит о том, что вы всем нутром хотите 1 результат.
Нет, сигнатура никак не запрещает вернуть 2 результата, ведь для этого она и создана.
Первые два результата будут отсутствовать.
Оборачивание в Try, Either, в юнион-типы, да во что угодно — бессмысленное синтаксическое усложнение кода.
Нет, это говорит о том, что у функции есть семантика. Большинство функций либо выдают (возвращают, бросают — не важно) ошибку, либо производят полезное действие (возвращают результат, не важно, одиночный или множественный, или произволят побочные эффекты). Соответственно, если мы получили от функции ошибку, нам не интересны возвращенные результаты (более того, в ряде случаев они опасны), поэтому если система типов позволяет явно сделать их недоступными — это уменьшает количество потенциальных ошибок.
Я же просил: конкретный пример. Вот прямо кодом, с конкретными типами.
Это если синтаксис таков, что вы не можете с этим комфортно работать. А в норме вы всего этого просто не видите, потому что автовывод.
У функции есть семантика, но она не говорит нам о том, что полезное действие должно быть одно.
Если с этим вы согласны, то почему функция должна решать, что является результатом, что ошибкой, что предупреждением, а что побочным эффектом?
Семантика функции лишь определяет ее возможности, но никак не оценку результата.
Если вы поймете теорию выше, мы перейдем к практике, обещаю.
Автовывод никак не решает проблему обратного разделения union-type (или любой вашей другой оболочки) от разворачивания в момент использования, т.е. он тут не причем.
SRP?
Потому что это заложено в ее контракт.
Отнюдь. Если в контракт функции заложено «я гарантирую, что будет возвращен корректный путь по графу, в противном случае вы не получите путь, а получите ошибку», то именно это и определяет оценку результата.
А что, просто так вы написать конкретный пример функции, выполняющей заданный контракт, вы не можете?
Какого обратного разделения? Какого разворачивания? Можете пояснить?
SRP и способ уведомления о результатах никак не связаны.
в языках с одним результатом — нет такой возможности, только «да или нет»
Вот оцените ваш русский язык,
А я лучше уж заложу возможность потери посылки, кражи, плохой логистики и заранее предупрежу об этом клиента. А он учтет каждый из этих вариантов в своем бизнес-процессе. А не будет сидеть и ждать ошибки
Я вам написал (int, int)
А как вы в языке с множественными результатами (только если можно, приведите конкретный пример на конкретном языке) опишете контракт функции вида «путь или путь не найден или найден отрицательный цикл» (все «или» — исключительные)?
Первый вариант, тот который и задумывался — семантическая инкапсуляция, когда мы, ради читабельности на естесственном языке, подразумеваем под одним словом различные реальные действия, т.е. полиморфизм.
Второй вариант — из-за невозможности вернуть 2 результата одновременно (а желание возвращать один результат исходит из неправильного понимания слова «функция»), мы объединяем результаты не ради семантики, а потому что технически невозможно по другому. Чаще всего при этом рождается монстр-переменная или функция, состоящая из 2 слов Результат1ИлиРезультат2 или еще более непрозрачные термины, типа GraphResult. Единственная цель второго варианта в 99% случаев, тут же распаковать (выполнить) наш монадический контейнер и достать Результат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
let (res, warnings) = giveMeWarnings()
Опять же, «предупреждения» и «ошибки» тем более друг друга не исключают. [...] Опять же, что считать ошибкой, а что предупреждением, решать должен вызывающий код, а у вас получается это решает вызываемый.
let res = getConfig()
when FileNotFound res = makeDefaultConfig()
when error is ValueIsEmpty return log( error ).ValueType.default
Статического анализа вызываемых методов вполне достаточно, чтобы вывести пользователю список возможных кейсов.
Чем больше, тем лучше.
Скрытие информации — один из способов реализации абстракций
который создаёт лишь дополнительные проблемы, когда требуются разные уровни абстракций.
Яркие примеры, когда нужен низкий уровень абстракций — тесты, обобщённая сериализация.
Ну, в качестве антипаттерна такой принцип есть, да
Сокрытие информации ни коим образом не поможет вам в борьбе со сложностью
При этом скрывая информацию вы создаёте себе сложности, когда вам нужно знать детали реализации.
Вот именно, что тесты работают с реализацией и для полноценного её тестирования требуется доступ к «приватным» членам.
Типичный способ обойти сокрытие информации — запихивание тестовых сценариев напрямую в тестируемый класс в качестве методов.
Обобщённая сериализация использует более низкоуровневые абстракции, раскрывающие всё внутреннее состояние, вплоть до тупого, но наиболее эффективного дампа памяти.
Доступность информации тоже не отнимает время на обработку.
Речь о программном доступе. Абстракции всегда текут.
Как протестировать инкапсулированный в объекте кэш исключительно через публичный интерфейс?
interface IURI { string getParam( string name ) }
Отвратителен, но работает. А что за замечательный способ есть в C#?
Как раскрытию информации мешает её сокрытие?
Существование википедии наверно вообще мозг взрывает?
URI инициируется строкой, при запросе getParam он лениво парсится и полученная внутренняя структура помещается в кэш. Таким образом последующие вызовы getParam происходят гораздо быстрее. Как вы реализуете этот класс и как протестируете, что повторный вызов getParam минует парсинг?
Ну да, рефлексия, паблик морозов и другие костыли для обхода проблемы, созданной безудержной манией сокрытия данных.
А кому не нужны — могут ими не пользоваться.
Так вас никто и не принуждает всё знать.
Ну вот, вместо одного простого юнита мы имеем уже 3,
При этом парсер попадает ещё и в публичный интерфейс, то есть, ради тестов мы раскрываем информации больше, чем необходимо пользователю.
Сокрытие данных в этом случае приводит к неоправданному увеличению сложности
эфемерное «может уменьшить расходы на модификацию программы в четыре раза»
Каким образом наличие доступа к информации усложняет рефакторинг видимо так и останется без обоснования.
Возможно потому, что в C# всегда можно обойти сокрытие, через субклассинг, рефлексию и прочее.
А вот в том же JS, сокрытие фиг обойдёшь.
Только вот задачи меняются, а допиливать инструменты специально под меня никто не будет.
URI, URIParser, MockURIParser. Наверняка ещё и URICacher захочется вынести. Ах да, ещё и URISerializer. Сделаем звездолёт ради священного «хорошего дизайна»! То есть вы предлагаете ещё и URIFacade сверху прилепить, чтобы скрыть получившуюся «правильно задизайненную» сложность?
IUriParser, делаем реализацию, которая всегда парсит, затем делаем кэш (с таким же интерфейсом), в который и заворачиваем. А моки никто не считает, их реализовывать уже не надо.Я просто проверю 3 кейса: parseURI формирует правильную структуру; getParam берёт данные из этой структуры; если структура не создана, то getParam её создаёт.
Там сравнивается монолитный и модульный дизайн.
При чём тут сокрытие информации?
Если оно используется, значит оно не избыточное.
Например, в питоне все поля публично доступны, но благодаря соглашению об именовании все знают, что полям начинающимся с подчёркивания стоит предпочитать поля начинающиеся с буквы. Но при этом никто не вставляет в палки в колёса и если программисту нужно вывести в лог красивый дамп объекта — это делается элементарно, без необходимости в каждом объекте реализовывать интерфейс IPrettyDebugString.
Ну да, всё, что не вписывается в догму сокрытия данных просто объявляется плохим дизайном
благодаря тому, что прячет сложность за простым интерфейсом — именно это нужно обычным программистам, а не информационные шоры.
1. Разделять IURIParser и IURISerializer — глупо, ибо они неразрывно связаны. Поэтому они должны быть частью одной абстракции — IURI.
2. Формат структуры (привет, пародия на ооп в виде data-object) у вас внезапно становится частью публичного интерфейса.
3. Парсинг опять же может происходить в несколько ленивых шагов — сначала парсим URI, потом парсим queryString. Прикрутим IQueryStringParser? В моём случае вся эта сложность инкапсулируется в одной абстракции — IURI.
IURI.В вашем случае моки всё же придётся реализовать ввиду специфичного поведения.
Для внешнего кода не менее важно, что он может спокойно обращаться к 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).
Я вам открою страшную тайну, но для программирования нужно знать куда больше абстракций, чем только лишь ООП, и соглашение об именовании *в рамках языка* — сущий пустяк.
Весь этот тред и вырос из моего тезиса, что инкапсуляция сложности и скрытие информации — разные вещи, а не синонимы, как многие считают.
data Maybe a = Just a | Nothing
parseInt :: String -> Maybe Int
parseInt s = ...
По-моему, lair под union type имел в виду не то, что вы подумали, а тип-сумму (в том смысле, в котором это понимает теория типов), или алгебраический тип данных.
Никакой динамической типизации это и не снилось.
Не надо изобретать велосипед, эти вещи вполне себе известны давным давно и успешно применяются.
Конечно, проще вернуть два значения (а если захочется 3? какое из них будет ошибкой?
И раз уж вы так уверены, что возврат ошибки превращает чистую функцию в нечистую
расскажите мне, нужна ли коммутация с внешним миром парсеру?
Стоит начать с того, что АТД были созданы не только для возврата значений.
Я даже думаю, что об этом вообще не думали, эта возможность получилась вполне естественно вытекающим из системы типов образом
Согласитесь, что отсутствие значения (когда мы возвращаем одно значение, другого у нас просто нет, оно не null, не nil; оно отсутствует и взять его не откуда) и специальное значение, которое по договорённости означает отсутствие этого самого значения — вещи несколько различающиеся (не говоря уже о том, что второе вообще звучит, как бред).
«Ошибки — это значения» в Go и эхо VB