Comments 31
Spring могут и доработать.
Нет смысла спешить с мнением, кмк.
С моей точки зрения это более явный и более удобный optional. Только в разрезе не только null safety.
Так же могу накинуть минус: кто-то может сказать что это checked exception 2.0.
Потому что позволяет одновременно проверять насколько ошибок в более удобном "try-catch (when)".
Но для меня это плюс, хоть и тоже самое, но читать такой код как в примере на презентации крайне приятно.
Нет, это совсем не checked exceptions. Это попытка уйти от исключений в принципе. Кроме того, написать лямбда функцию с checked exception невозможно. А с такой возвращаемой ошибкой - пожалуйста. Исключения - одна из родовых травм Java, и всем JVM-языкам приходится возиться с ними, к сожалению.
Родовая травма языка, это отсутствие исключений, когда они физически присутствуют в любой компьютерной программе, но разработчики изобретают разные способы их сокрытия, чтобы не заниматься из обработкой
Передача значений из оперативки в регистры процессора физически присутствуют в любой программе. Или "это другое"?
Не понял, причем тут передача значений из оперативки в регистры
Та же самая история - если что-то присутствует на низком уровне, почему его надо тащить на высокий?
На низком уровне присутствует и движение электронов, однако на более высокий уровень имеет смысл тащить только те элементы, которые так или иначе используются в алгоритмах.
Прерывание потока выполнения при возникновении определенной ситуации (не обязательно ошибки), это один из базовых элементов работы практически у всех вычислительных машин и самый простой способ обработки возникающих в программе ошибок. Но рациональность использования этого в ЯВУ, уже другой вопрос.
Удобно видеть, какие ошибки могут возникнуть при вызове метода. Не удобно, что они возвращаются функцией, обработка будет выглядеть странно. В try catch ты получаешь явно объект ошибки, думаю поэтому его и придумали. Как будто это шаг назад
Должна быть функциональная обвязка, как map/map_err в Rust. Тогда можно будет строить цепочки обработчиков.
Не удобно, что они возвращаются функцией, ...
?! Как раз - наоборот. В кои-то веки, "обшибки" - по честному - часть сигнатуры ф-ции. С нормальной композицией, выводом и т.п.
Если не понятно - User | NetworkError из примера - это полноценный тип. Эдакий AA union type, с единственным ограничением: AA это исключительно для подтипов Error.
Оно - может быть - и не универсально. Зато - сильно утилитарно получается. Как в Haskell - может быть и хочется, но не получится по очевидным причинам. А как в Scala - ну мы видели, к чему "оно" приводит :-)
... обработка будет выглядеть странно
Да ладно?! Даже самый очевидный (но не единственный) вариант обработки - через when выглядит очень органично.
В try catch ты получаешь явно объект ошибки, думаю поэтому его и придумали.
Тут у нас отдельный высший тип для ошибок - Error. Т.е. они ("обшибки") - by design - не пересекаются с подтипами Any? - куда уж явней "объект ошибки" получать?
Единственное преимущество Throwable - это наличие stacktrace. Но и тут - если оно нам таки надо - всегда можно "раскрутиться" через !! оператор.
Как будто это шаг назад
В каком месте?!
А как в Scala - ну мы видели, к чему "оно" приводит :-)
Я не видел. И к чему же?
Если коротко - каноничный AA-union-type штука сильно уж - скажем так - "обобщающая". Во всех смыслах этого слова.
И очень уж много нужно контроля (на уровне "усилий мозга"), чтобы пользоваться этим "правильно". А чуть "отвлекся" - и всё... "получилась какая-то нечитаемая хрень" :-(
И ещё больше усилий нужно для возврата в "контекст". Читать "это", порой, совсем "грустненько"... хотя и сам "это вчера только написал" :-)
А можно пример "нечитаемой хрени"?
Видимо, такое
// Используем стандартный тип Either[A, B] для представления "или-или".
// Договоримся, что слева (Left) - ошибка, справа (Right) - успех.
// А как представить "Загрузку"?... Уже проблема.
// Ну, допустим, обернем всё в Option.
type WebRequestResult[T] = Option[Either[String, T]]
// None => Загрузка
// Some(Left("...")) => Ошибка
// Some(Right(data)) => Успех
Использование
def printResult(result: WebRequestResult[Int]): Unit = {
result match {
case None => println("Загрузка...")
case Some(either) => either match {
case Left(msg) => println(s"Ошибка: $msg")
case Right(data) => println(s"Успех! Данные: $data")
}
}
}
один разработчик использует "каноничный" sealed trait, другой — Either, а третий придумывает свою структуру на tuple'ах. Все три подхода работают, но вместе превращают кодовую базу в ту самую "нечитаемую хрень
Ну, здесь Option - вообще лишнее. Есть Future/Promise в стандартной библиотеке, Deferred в Cats, что-то похожее в ZIO. Это все композируется в более эффективные конструкции, чем регулярный опрос состояния. Посмотрите на такой вариант:
future andThen {
case Left(msg) => println(s"Ошибка: $msg")
case Right(data) => println(s"Успех! Данные: $data")
}
Возможно, вам стоит пересмотреть архитектуру приложения?
Но если уж так хочется разделить три этих случая - алгебраические классы к вашим услугам. Если вы в третьей Scala, используйте enum. Во второй - три case-класса с общим предком. Типа такого:
trait Status[+T] {
def print(): Unit
}
object Status {
case object Running extends Status[Nothing] {
def print(): Unit = println("Загрузка...")
}
final case class Success[T](data: T) extends Status[T] {
def print(): Unit = (s"Успех! Данные: $data")
}
final case class Failure[T](msg: String) extends Status[Nothing] {
def print(): Unit = println(s"Ошибка: $msg")
}
}
:-) "Нечитаемая хрень" - это многоэтажные match'и от которых в принципе не уйти, как только union types начинают "оккупировать наш код".
@Dhwtj уже привел пример... он не слишком удачный, конечно - там вообще нет union... и это всё - более-менее - "съедобно", но "идея" - я думаю - должна быть ясна :-)
Зачем вообще нужен union-type? Очевидно, чтобы выражать "штуки" типа
t<T> -> t<t<T>> -> t<T>
Условно, ну хочется мне чтоб
Option<Option<T>> = Option<T>
и "вот это вот всё", что в принципе не выражается через sum-type.
И тут же вылезает "проблемка"... union-type - by design - некомпозициональный тип. Это - само по себе - значит не мало, но - ко всему прочему - означает так же, что "номинативно" работать с элементами такого типа - неудобно. А со структурной типизацией у jvm возможности, прямо скажем, весьма ограничены. Соответственно, мы гарантированно проваливаемся в match там, где - казалось бы - это и не нужно.
Да, можно сослаться, например, на какой-нибудь reflectiveSelectable... и прочее. Но все эти "штуки" - жрут как не в себя и - соответственно - "на наш путь" :-)
Ну и пошло-поехало... Мыж умные. Мыж не просто так, выбрали union (а не sum, например). У нас причина есть :-) И с огромной долей вероятности, элементами нашего супер-пупер-AA-union-type будут оказываться не менее гига-мега-AA-union-type и т.д. Ну это просто действительно удобно - обобщать через union.
Но вот разбираться с результатом этого обобщения (полученного, например, в результате композиции) в отсутствии структурной типизации - мягко говоря, тяжко.
А чтоб "не было тяжко" - приходится делать над собой усилие и бдеть :-) И других вариантов, к сожалению, просто нет.
При этом, то, что предлагают ребята из Kotlin - лично для меня - не выглядит сколь-нибудь страшно. Просто по той причине, что все элементы, в предлагаемом типе (кроме одного... которого может и не быть) - по определению: имеют эквивалентную семантику (это ошибки), не могут образовывать собственной иерархии (они не open, не могут реализовывать интерфейсы и не могут быть параметризированы) и не имеют пересечений с остальными типами (у них отдельный top type). Т.е., грубо говоря, самая "страшная" часть этого union - гарантированно "плоская". А "как есть таких слонов" (tm) - мы прекрасно знаем.
Да - остается опасность "обобщающего элемента" (в слайдах про "это" ничего нет). Но, я думаю, что там парни "не глупее нас" - самое простое, имхо, тупо запретить включать сам Error в множество.
P.S. На всякий... честно - мне лень тут приводить листинги чтоб "на пальцах" продемонстрировать вам, то, о чём написано выше. Имхо, всё что выше - это и так жуткий оффтопик. Но, судя по всему, scala для вас "родное", а значит - вы и сами должны прекрасно себе представлять все эти "прелести".
Ну и кстати, в rust наиболее канонично видимо так
use std::path::PathBuf;
// Ошибка #1: не удалось прочитать конфиг
#[derive(Debug)] // для вывода
pub struct ConfigError {
pub path: PathBuf,
pub reason: String,
}
// Ошибка #2: не удалось подключиться к БД
#[derive(Debug)]
pub struct DbError {
pub host: String,
pub port: u16,
pub message: String,
}
// Функции, которые их возвращают (заглушки)
fn load_config() -> Result<(), ConfigError> { /* ... */ Err(ConfigError { path: "cfg.toml".into(), reason: "permission denied".into() }) }
fn connect_db() -> Result<(), DbError> { /* ... */ Ok(()) }
Использование
use anyhow::Result;
fn run_anyhow() -> Result<()> {
load_config()?;
connect_db()?;
Ok(())
}
fn main() {
if let Err(e) = run_anyhow() {
// e - это `anyhow::Error`, который хранит исходную ошибку внутри
println!("Ошибка: {}", e);
// При желании, можно добраться до исходного типа
if let Some(config_err) = e.downcast_ref::<ConfigError>() {
println!("Проблема с конфигом: {}", config_err.path.display());
}
}
}
Теперь почти как в rust))
Интеграция с фреймворками: Самый веский практический аргумент. Этот подход лучше всего работает в ядре бизнес-логики, изолированном от фреймворка. На границе (в контроллерах) пишется адаптер: when (result) { is Ok -> ..., is Err -> mapToHttpError(...) }. Пытаться тянуть Result до самого Spring MVC — плохая идея
Пытаться тянуть Result до самого Spring MVC — плохая идея
"Прикол" в том, что тут как раз нет Result'а. Ни в терминах rust'а, ни в терминах kotlin'а.
Result - это "эвфемизм" более общего Either. А Either - by design - скажем так, "хромает" в плане композиции "левой" своей части. Т.е. "левая" часть композиции выводится в что-то осмысленное только при прям очень сильных ограничениях, накладываемых на. Чего - по понятным причинам - делать вообще не хочется.
Result - соответственно - "хромает" на "правую" свою часть.
То, что предлагается ребятами из kotlin - позволяет с одной стороны - избежать такого рода "хромоты". А с другой - не скатится в AA-union-hell.
Насколько - в реальности - окажутся сильными ограничения, накладываемые на подтипы Error - это будем делать посмотреть (с). Дизайн пока не финализирован. Но то, что есть сейчас не выглядит сколь-нибудь "страшным".
Я к тому, что иметь - условный -
private val <T:Any> (T|Error).responseEntity: ResponseEntity<out Any> =
get() -> when { ...}
на уровне контроллера (или даже его пакета) - не выглядит, имхо, чем-то прям "ужос-ужос". Error и upper-bound - понятно, "в реальности" заменяются на какие-то более осмысленные type aliase'ы.
А вот как раз "городьба" отдельной иерархии для передачи "типизации в ошибках" в контроллер - при условии наличия rich errors - будет выглядеть "немножко странно". Нет?
Да, в этом смысле удобнее.
Тип возврата становится объединением всех возможных ошибок, а не сложной иерархией. В раст нет анонимного типа объединения, его надо заранее создать. А в Котлин оно будет анонимным, создаваемым автоматически
За последние несколько лет я запилил десятки контроллеров. По итогу пришел как раз к примерно такой схеме - сервис, который обслуживает контроллер, всегда возвращает Either. Может, конечно и исключение бросить, но это всегда 500.
Сначала были исключения на все случаи, но потом оказалось, что нет надежного способа заставить в юнит тестах контроллеров из моков сервиса кидать те же исключения, что кидаются из настоящих.
Сугубое имхо...
Выглядит оно уже сейчас сильно "вкусно". Ребята из arrow - уже облизываются :-) Наконец-то нормальная "человечья" error composition "из коробки".
Озвученные "сомнения" - в разрезе Kotlin - выглядят, мягко говоря, неубедительно.
Яб таки дождался финализации. Возможно, оно действительно "негативненько" скажется на interop'е со стороны Java. Возможно (ну а вдруг), придумают как таки обойтись - в этой части - без "костыликов". Но это пока единственное, что хоть сколько-то "пугает".
Какой-то недо typescript. Тулбокс для всего этого маленький, зачем возвращать ошибку если можно было бы возвращать разные классы например. Типов нет, а семантика как будто есть. Котлин стал не лучшей джавой, а недо сишарпом.
Есть где-нибудь нормальное краткое описание фичи? 45-минутное видео смотреть не горю желанием.
Не понял как "кидается" ошибка. Через throw или через return? Могу ли я создать функцию, которая возвращает и кидает ошибку того же класса одновременно? Если да, то как понять был ли это успешный возврат или ошибка?
перебирай типы, в котлине не в моде полиморфизм. И не задавай лишних вопросов, а радуйся, радуйся!
Есть где-нибудь нормальное краткое описание фичи?
Не понял как "кидается" ошибка. Через throw или через return?
throw никак не меняется. Соответственно, не имеет смысла для подтипов Error. Т.е. "ошибка" возвращается штатно - через return.
Могу ли я создать функцию, которая возвращает и кидает ошибку того же класса одновременно?
Нет. Но если очень хочется, через !! можно кинуть KotlinException, который "обернет" Error.
Если да, то как понять был ли это успешный возврат или ошибка?
Все "ошибки" - это подтипы Error. Error - это новый отдельный top type. Т.е. и он, и его подтипы не приводятся к Any?. Соответственно, на "или ошибка" можно проверять, банально, через is Error.
Ну т.е. аналог того, что мы влзващали бы ошибку или результат как варианты sealed класса, только теперь меньше кода надо писать, если ошибки одинаковые в разных методах. В целом неплохо, но и не то, чтобы прямо интересно. У exception уникальная механика пробрасывания ошибки вверх по цепочке вызовов и finaly гарантирующий выполнение, а тут по сути просто синтаскический сахар.
Получится так, что придётся ловить и старые исключения, и обращать внимание на возвращаемые ошибки. Ведь никто не будет переписывать всю библиотеку классов, чтобы функция чтения файла стала возвращать все возможные виды ошибок.
Особенно "приятно" будет писать высокоуровневый код.
Например, по бизнес-логике функция getUser(int id)
возвращает User
или UserNotFoundError
, теперь в сигнатуру придётся тащить всё, что у неё под капотом: если юзер читается из БД, то и все database-ошибки и ошибки сети, если из файла - то все файловые ошибки (включая отсутствие прав). Если что-то меняется слоем ниже, придётся глобально переписывать все слои вверх.
Ну кстати, если не специфицировать, какие конкретно исключения выбрасываются, а поместить в сигнатуру корень иерархии java.lang.Exception
, то абстракция (сигнатура функции) не будет зависеть от реализации. Если обработчику потребуется поклассифицировать ошибки, можно посмотреть на тип объекта Exception.
Что от этого выигрываем? Удобнее комбинировать операции в функциональном стиле (монады), меньше расходы на выбрасывание и ловлю исключений (но теряем StackTrace, а иногда это интересно для разбора ошибок).
И что теперь, когда большой каскад вызовов методов, и каждый возвращает свой тип ошибки, как пробрасывать ошибку из глубины на самый верх, где она будет обработана? Чем это отличается от простого возврата составного класса Result, где одно поле - это нужный результат работы метода, а второе поле - класс ошибки, который должен возвращать этот метод? Теперь вызывающий метод, который вызывает методы трех разных классов, должен парсить все их результаты, смотреть, нет ли там ошибки, и если есть, делать new своего класса ошибки, упаковывать туда возвращенную ошибку и это значение return? А более высокий метод должен делать то-же самое? Спасибо. В свое время, исключения создавались, чтобы этого избежать.
Мы так и в Java делали - с возвращением кодов и сообщений ошибок.
Плохо понятно, чем это лучше exception handling, кроме разве что использования в функциональщине.
Rich Errors в Kotlin 2.4: шаг вперёд или шаг в сторону?