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

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

Этот ваш "try, запоминающий цепочку вызовов" давно придумали, монады называются.

Вообще вариантов, как сделать

прочитали из базы()
обработали()
записали результат()

придумали примерно два: либо через исключения, либо через монады. Они, конечно, стектрейс сами-по-себе не запоминают, но всегда можно сделать специальный вариант Either, у которого Left будет хранить хоть стектрейс, хоть вообще весь контекст.

Ещё можно через алгебраические эффекты.

А почему, кстати? Какие именно ограничения?

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

А это означает запрет инлайна, запрет оптимизации концевой рекурсии и т.п.

Хм, вроде бы Scala умеет оптимизировать хвостовую рекурсию в JIT, хотя и exception там есть. В Koltin тоже есть tailrec. Да и не так часто встречается хвостовая рекурсия.
Inlining в JIT тоже есть и работает.

Эксцепшенам самим по себе оптимизация не мешает, так как является эквивалентным преобразованием кода. Мешает именно интроспективному просмотру стектрейса.

Например, в C++ есть эксцепшены и де-факто есть оптимизация концевой рекурсии, но нет стектрейса. В Питоне есть эксцепшены и стектрейс, но из-за этого нет оптимизации концевой рекурсии.

Так stacktrace есть во всех указанных вариантах, и в Scala и в Kotlin и в Java.

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

Хм, а по сравнению с чем проблемы с оптимизацией в JIT?

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

Ну, в kotlin добавили tailrec для этой задачи. Но глубокая рекурсия и в C++ штука опасная и требует аккуратности.
Но тут, вроде бы, говорили про Go, а не про C++.

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

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

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

Именно так работает упомянутый Вами tailrec в Kotlin. Однако, если почитать полемику, то, похоже, практически никто из программистов на Kotlin не понимает точной семантики и назначения этой конструкции (отчасти в этом виновато неудачное имя, больше подошло бы что-то вроде nostackframe).

Однако искажение stack trace при оптимизации способно провоцировать труднообнаружимые ошибки.

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

Например, ключевое слово try перед вызовом функции

1 в 1 путь Rust с его макросом try!. Тогда сразу стоит сразу внедрять оператор ?, чтобы избежать конфликтов имен и избавиться от ада скобочек:
try(try(try(foo()).bar()).baz()) -> foo()?.bar()?.baz()?

ЗЫ дизайн-доки для закрытой дискуссии нагляднее.

if err != nil {

return fmt.Errorf("не смогли прочитать из базы: %w", err)

}

Или хотя бы позволяли писать как-то так:

if err != nil  return fmt.Errorf("не смогли прочитать из базы: %w", err)

В одну строчку? Или принципиально без фигурных скобок?

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

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

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

Ну не знаю, было

прочитали из базы()
if err != nil {
    return fmt.Errorf("не смогли прочитать из базы: %w", err)
}
обработали()
if err != nil {
   return fmt.Errorf("не смогли обработать: %w", err)
}
записали результат()
if err != nil {   
   return fmt.Errorf("не смогли записать результат: %w", err)
}

Стало

прочитали из базы(ctx)
обработали(ctx)
записали результат(ctx)
return ctx

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

Будут, поэтому внутри каждой функции должно быть вначале что то типа if(ctx.isFail()) { return ctx; }

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

Если бы можно было, то уже давно пресловутую конструкцию if err != nil сделали бы макросом или препроцессором.

Будут, поэтому внутри каждой функции должно быть вначале что то типа if(ctx.isFail()) { return ctx; }

И тогда мы получаем ту же самую конструкцию if err != nil, только заходим немного с другого конца :) Право на жизнь ваш концепт, несомненно, имеет, особенно в мире, где есть реализация обработки ошибок в Го. Но как по мне, всё же лучше прервать цепочку вызовов там, где она больше не может продолжаться, чем продолжить её выполнение в усечённом виде.

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

Под макросом я понимал дефайн как в C

А, понял. Не, Го не умеет в метапрограммирование, максимум на что он способен - это кодогенерация.

Но в целом я уже целый день думаю об этом, зачем-то

Потому что это интересно :)

Но дженерики то есть? Я вроде гуглил и были?

Это отлично, учитывая что по go у меня только книжка есть и небольшое желание его изучать)

Хотя глянул и это не совсем то

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

Верятно как в Hare было бы более удобно

https://harelang.org/tutorials/introduction#a-few-words-about-error-handling

Кажется, и в Rust и в Swift и в Zig применяется "легковесная" схема обработки ошибок с optional и either и оператором, который возвращает управление из вызывающей функции, если вызываемая функция вернула ошибку (префиксный try или постфиксный "?"). Данная схема выглядит весьма привлекательно и кажется, ее вполне можно имплементировать в Go. Но для этого в язык совершенно точно нужно протащить опционалы и Either. И интересно, как это можно совместить с принятой в Go схемой возврата двух значений - смыслового и кода ошибки, так чтобы старые функции без переписывания заработали по новой схеме?

В go 2 версии хотят для ошибок добавить конструкции check, handle.

А может просто вызывать метод или, если ошибка, исполнить другой код?

Например, как это сделано в V :D

https://docs.vlang.io/type-declarations.html#optionresult-types-and-error-handling

Пример из статьи выглядел бы так:

// вызываем функцию, а если ошибка - паникуем
data := read_from_db() or { panic(err) }

// ещё вызываем функцию, но необрабатываем ошибку если она есть - !
processed_data := process(data)!

// вызываем функцию, а если ошибка, то обрабатываем её в этом блоке - например, вызываем функцию do_something с передачей ошибки в качестве аргумента
write_result(processed_data) or {
  do_something(err)
}

и никаких вам if (err != nil) :>

Конструкция or

Может обработать ошибку и вернуть значение по умолчанию:

fn do_something(s string) !string {
    if s == 'foo' {
        return 'foo'
    }
    return error('invalid string')
}
​
a := do_something('foo') or { 'default' } // a будет 'foo'
b := do_something('bar') or { 'default' } // b будет 'default'
println(a)
println(b)

Может раньше прервать выполнение:

user := repo.find_user_by_id(7) or { return }

Может исполнить какой-то другой код:

user := repo.find_user_by_id(7) or {
  log("not found", err)
  return
}

и просто, и гибко :D

а это ещё не зашла речь про panic, recover и defer..

Зарегистрируйтесь на Хабре, чтобы оставить комментарий