Comments 19
В целом, да. Добавлю только, что проблемы с исключениями - чисто архитектурные и присутствуют во многих языках, где вообще встаёт вопрос "возвращать или ловить?". В своём коде всегда ловлю вызовы внешних библиотек, которые и так обычно в чистом IO a
работают, и непосредственно на верхнем уровне - в main или forkFinally, на случай, если где-то всё же упустил. Все внутренние вызовы всегда вокруг ExceptT/MonadError построены.
Хорошая, вдумчивая статья (не всю пока прочитал — в процессе). Давно не читал на Хабре чего-то, что мне бы понравилось. Спасибо!
Хорошая статья, спасибо!
Жаль, что на Хабре про Haskell пишут крайне редко.
Автору -- респект и пожелание писать ещё!
Хотя, например, в Rust, такой проблемы нет, однако и там сообщество пришло к выводу, что тамошний
Result
из коробки тоже непригоден к использованию.
Не понимаю, о чём вы, но кликбейт неплохой. Если с чем и есть некоторые проблемы, так это со стандартным трейтом Error. Но никто не говорит, что надо его использовать везде.
Систему с раскруткой стека и паниками мы не называем исключениями и (кроме весьма специфичных случаев) не ловим и не обрабатываем. Если приложение паникует и умирает, то это не ошибка, а баг в коде. По этому поводу есть консенсус в сообществе.
Опять же в 95% случаев ошибка это просто информация о некорректных входных данных (в довольно широком смысле). И всё, что вы можете сделать в этом случае - внятно сообщить об этом пользователю.
Кстати, полагаю, сейчас центральным местом в обработке ошибок становится даже не сам Result, а трейт Try. При этом Result всё ещё остаётся подходящим для подавляющего числа случаев. Для некоторых других случаев есть Option и ControlFlow.
Проблема Result в том, что когда они используется, как есть (без box Error, failure, anyhow), то он не композится с другими Result. Именно поэтому и придумали failure и anyhow. Однако их проблема уже в другом. Как только ты забоксил ошибку в трейт Error (или обернул в anyhow::Error), ты понятия не имеешь, что за ошибка лежит внутри. Два стула: либо не композится, как MonadError/ExceptT либо неизвестность, как с голыми исключениями.
Если не ошибаюсь, anyhow::Result
используется только в приложениях и конечном киентском коде, поэтому его и композить нет необходимости. И не очень понятно, как предлагается композить два чёрных ящика, кроме как создать ещё один ящик и положить эти два туда. Вот этот момент мне не совсем понятен.
Добавлю, чтобы не писать лишний комментарий: хорошая информативная статья ?
Наверное, я как-то криво выразился. Не нужно композить два чёрных ящика (box Error, с anyhow::Error). Нужно композить два белых (конкретные Result<FooError, _>, Result<BarError, _>) в один чёрный. И как только вы это сделали, объединили Result<FooError, T> с Result<BarError, T> в Result<anyhow::Error, T>, вы потеряли вообще всю информацию, какие конкретно ошибки могут там быть. Это всё равно, что хаскелевый SomeException
Он без проблем композится с другими Result если настроено отношение From.
Оператор ? это по большей части сахар для следующей конструкции (версия для Result):
match some_expr {
Ok(ok_value) => ok_value,
Err(err_value) => return From::from(err_value),
}
Обратите внимание на вызов From::from. Это вызов преобразования из одного типа ошибки в другой. Так что для того, чтоб композитить это всё, нужно только реализовать From для своего типа ошибки. thiserror делает этот процесс простым и приятным. При этом не теряя явности.
А для случая, когда нужно кастовать один Result в другой Result есть такие варианты:
fn foo<T>(result: Result<T, A>) -> Result<T, B> {
Ok(result?)
}
fn bar<T>(result: Result<T, A>) -> Result<T, B> {
result.map_err(From::from)
}
impl From<A> for B {
fn from(a: A) -> B { .. }
}
// не существует реализации impl<T, A, B: From<A>> From<Result<T, A>> for Result<T, B>
// причина тому конфликт с impl<T> From<T> for T
В случае anyhow можно посмотреть внутрь при помощи downcast, в случае thiserror будет работать стандартный match. Крейт failure слишком устарел для того, чтоб упоминать его.
Можно equational constraint добавить
А куда ты его добавишь-то?
Я, кстати, не вижу фундаментальных причин, по которым
HasCatch
изcapability
Функциональные зависимости всё портят. Хотя в этом случае я даже не особо понимаю, как оно там считается
data Foo = Foo deriving (Show, Exception)
data Bar = Bar deriving (Show, Exception)
throwCap :: forall a m b. Cap.HasThrow a a m => a -> m b
throwCap = Cap.throw @a
type CanThrow a = Cap.HasThrow a a
foo :: (CanThrow Bar m, CanThrow Foo m) => m ()
foo = do
() <- throwCap Foo
throwCap Bar
catchCap :: (Unlift.MonadUnliftIO m, Exception e) => Cap.MonadCatch e m a -> (e -> m a) -> m a
catchCap (Cap.MonadCatch m) = Unlift.catch m
baz :: CanThrow Bar m => m ()
baz = foo `catchCap` \Foo -> pure ()
• Couldn't match type ‘Foo’ with ‘Bar’
arising from a functional dependency between:
constraint ‘HasThrow Bar Bar (MonadCatch Foo m)’
arising from a use of ‘foo’
instance ‘HasThrow tag e (MonadCatch e m1)’ at <no location info>
• In the first argument of ‘catchCap’, namely ‘foo’
In the expression: foo `catchCap` \ Foo -> pure ()
In an equation for ‘baz’: baz = foo `catchCap` \ Foo -> pure ()
|
xxx | baz = foo `catchCap` \Foo -> pure ()
| ^^^
Для зависимых типов нужна будет проверка тотальности. И с ней будет трудно писать программы. Именно прикладные программы. Идрис ставил себе целью совместить эти две вещи. Посмотрите, что получилось. Может быть, вам он нужен, а не Хаскель.
Нет, в Haskell неудобно обрабатывать ошибки (если не пользоваться эффектами)