LINUX.ORG.RU

Haskell и обработка исключений.

 , ,


1

1

Вот у меня такой странный вопрос: почему IO функции выбрасывают ошибки ? Я имею в виду зачем делать error в IO вообще ?

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

В документации по хаскелю сказано, что выбрасывание ошибок нужно делать, когда дальнейшее выполнение программы не имеет смысла. Например ошибка паттерн матчинга - есть ошибка программы, ведь ADT в хаскеле - практически часть алгоритма, и поэтому единственным способом избавления от ошибки является правка кода. Все остальные ошибки, к которым относятся все ошибки ввода-вывода являются не ошибками, а исключениями, и должны обрабатыватся вещами типа EitherT.

Ну действительно: разве это ошибка, когда файл не удается прочесть ? Разве должна программа из-за этого падать ? Я понимаю, что есть средства обработки ошибок, но эти средства позволяют лишь отловить ошибку и сделать некие действия, но не более. Ведь если прерывать вычисление, то теряются все монадные профиты.

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

ignoreErrors :: [EitherT IO a] -> IO [a]
ignoreErrors x = do
  y <- sequence $ map runEitherT x
  return $ rights y
При этом монада нам гарантирует, что никакая ленивость не помешает нам отловить все ошибки ввода-вывода которые могут возникнуть, что все монадические действия вычислятся как минимум до конструктора Either, ведь могут существовать и другие исключения, не только те, что связаны с вводом-выводом, но и некоторые исключения в вычислениях. Все это будет отловлено в тот момент, когда мы просто паттернматчим результат runEitherT с Left или с Right.

Если задача стоит просто выполнить действия и гарантировать, отсутсвтвие исключений во всех вычислениях, то это просто sequence или sequence_, в зависимости от того, нужен нам результат вычислений, или нет.

Обрабатывать ошибки с помощью отлова ошибок в тотально ленивом языке - не самая классная идея, ведь у нас нет гарантии вычисления выражения до той глубины, в которой станет ясно, что ни одна ошибка больше не выбросится, хаскель не отслеживает использование error и не форсирует ее вычисление до тех пор, пока это не станет нужно для другого какого - нибудь вычисления.

Отсюда у меня вопрос: о чем думали создатели хаскеля, когда вообще включали error в стандартную библиотеку, использовали ее во всех функциях (деление на ноль) и давали пользователю ей пользоваться. Взгляните на все библиотеки, они все используют выбрасывание ошибок, вместо обработки исключений. Либо я чего-то не понимаю, и выбрасывать ошибку - очень удобно и правильно, либо это фейл проектировщиков ?

Дискач.

PS. haters gonna hate

Инкрементирую. В данный момент с этим же вожусь.

dmfd ()

Для обработки исключений в этих моих хаскелях используют специально обученные монадные трансформеры куда можно запихнуть IO например. Использовать error плохо и не очевидно. Однако возникает два вопроса: что должно происходить когда кто-то делит на ноль и что делать тебе в своём хорошем коде, если в библиотечном коде кто-то упорно делит на ноль?

KblCb ★★★★★ ()
Ответ на: комментарий от KblCb

Обрабатывать ошибку, это же очевидно. А функция деления вообще должна возвращать Either так же как и head. Вопрос то состоит в том, оправдан ли вообще такой подход, когда пользователя ленивого языка может делать error.

s9gf4ult ★★ ()
Ответ на: комментарий от KblCb

Все вычисления, которые могут фейлиться. Это и деление и открывание файла и работа с базой данных. Да. Естественно есть вычисления которые никогда не фейлятся, а лишь выбрасывают ошибку, но тогда это ошибка, и требует внимание программиста, а не пользователя. Как я уже сказал, оперировать со значениями Either гораздо проще - можно использовать категориальные профиты, как в других монадах.

s9gf4ult ★★ ()
Ответ на: комментарий от s9gf4ult

Нет уж, для целочисленных вычислений лучше nan ввести. Вычислять выражения в ErrorT — лучше, мне кажется, застрелиться, чем так жить.

dmfd ()
Ответ на: комментарий от s9gf4ult

бред.. нетотальные функции часто нужны, если дело не доходит до agda, а если доходит, то почему бы не использовать её. Для желающих чрезмерной безопасности есть модуль Safe.

qnikst ★★★★★ ()
Ответ на: комментарий от dmfd

Не ErrorT, а Either для не монадических функций и EitherT для монадических. Все просто, да и где сложность в арифметике с монадами/функторами ? Разве что в многословности.

s9gf4ult ★★ ()
Ответ на: комментарий от s9gf4ult

в многословности, в том что у тебя лишний уровень indirection, в том, что у тебя компилятор нифига не сможет сделать примитивные значение где может. Нафига оно надо? Плюс пойдет куча решений,а какой Either делать Either String? Either Text? Either MyFooFunctionError? а потом извращаться их объединять и мапить др на друга?

qnikst ★★★★★ ()
Ответ на: комментарий от qnikst

Ну ладно, с чистыми вычислениями все ясно, там и правда не так все гладко. Но выбрасывать Exception в операциях ввода-вывода - это же нехорошо, ведь там нужно форсировать вычисление error (или пользоваться методом fail из IO) чтобы прекратить вычисление действия и не совершить следующее IO действие, даже если текущее вызвало исключение. Нужна же гарантия последовательности вычислений, а это монада. Я не прав ?

s9gf4ult ★★ ()
Ответ на: комментарий от s9gf4ult

C Lazy IO так не выйдет. Думаю, что возможна, например, такая ситуация: файл успешно открыли, что-то прочитали, а к тому времени, как стало нужно прочитать ещё кусочек, диск сдох.

dmfd ()
Последнее исправление: dmfd (всего исправлений: 1)

когда вообще включали error

Не путаешь с userError?

Либо я чего-то не понимаю, и выбрасывать ошибку - очень удобно и правильно

Если все-таки не путаешь, то можно конкретный пример, где это так злоупотреблено? А то, например, я не знаю как обрабатывать head [].

anonymous ()
Ответ на: комментарий от s9gf4ult

Не обязательно именно так, можно, скажем лифтить в пользовательскую монаду, где по вкусу определены соотвествующие fail. Есть пакет на hackage.

Проблема скорее в том, что нет единого подхода, а местами перекрывается.

anonymous ()
Ответ на: комментарий от dmfd

head :: [a] -> Maybe a

Батенька, это уже не head. Никто не запрещает import Safe, где это удообно. Но предложенное решение может усложнить без особого профита.

anonymous ()
Ответ на: комментарий от dmfd

И да, где связь-то между `error' и IO, ткните что ль.

anonymous ()
Ответ на: комментарий от rand

Ты отвечал на топик, он вбразывал про «целочисленное» постом выше. Затем он возразил в _своем_ контексте.

anonymous ()
Ответ на: комментарий от anonymous

Не путаешь с userError?

Да, выше выяснилось, что ленивый error таки нужен, для оптимизаций и быстрого прототипирования. Нормальный чистый код конечно не выбрасывает error где попало, а использует Either или Maybe. А вот

fail :: IO a
Названа очень удачно.

s9gf4ult ★★ ()
Ответ на: комментарий от s9gf4ult

ты что-то сильно путаешь или плохо описываешь - операции ввода вывода это IO, там и exception и error вывалится, исключение только try (return ioWithError) >>= id, но в данном случае нужно понимать, что делаешь и все последовательно. Если ты внутри IO создаешь ленивое вычисление, которое может свалиться, то тогда можно всегда сделать evaluate/deepseq, но это опять же не относится к IO. Можешь привести пример тут или juick, который кажется тебе кривым, чтобы убедиться, что мы друг друга до конца понимаем?

qnikst ★★★★★ ()
Ответ на: комментарий от dmfd

Prelude заслуживает того, чтобы быть выкинутой полностью, учебная либа не нужна:) lazy io на самом деле дает интересный функционал и удобные абстракции, при этом надо понимать где могут быть проблемы.

qnikst ★★★★★ ()
Ответ на: комментарий от qnikst

Prelude заслуживает того, чтобы быть выкинутой полностью, учебная либа не нужна:)

Тоньше надо

anonymous ()
Ответ на: комментарий от anonymous

Программистам на других языках возможно кажется, что хаскелеры решают надуманные и несущественные проблемы, но зачем тогда нужен хаскель, если не делать все правильно Just Completely ?

s9gf4ult ★★ ()
Ответ на: комментарий от s9gf4ult

За тем же, зачем и любой другой нормальный язык - решать практические задачи, а не выдуманные.

anonymous ()

Ты прав в целом, но не прав в частностях.

Во-первых, C-функции вполне способны бросать исключения. Собственно, с точки зрения операционки C-функции от C++-функций не отличаются вообще ничем.

Во-вторых, далеко не всегда Either - правильный выход.

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

Посреди вывода функция может гавкнуться. По твоей идеологии она должна возвращать Either SomeException String. А вот фигушки, потому что при попытке сделать паттерн-матчинг по такому выводу придётся всосать в память всю строку целиком (дабы убедиться, что оно таки нигде не гавкнулось). А это, вполне возможно, приведёт к падению программы с ошибкой переполнения стека. Сам на такое наталкивался.

Правильный вариант в данном случае - возвращать (String, Maybe SomeException) - то есть, прочитанную часть плюс возможно возникшее исключение. Ещё более правильный путь - воспользоваться библиотекой conduit и превратить функцию в Source. Но никак не Either SomeException String.

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

Miguel ★★★★★ ()
Ответ на: комментарий от Miguel

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

Во-вторых, Either вариант ничем не уступает кондуитам, пример: если поток у тебя - это String, то функция возвращающая один элемент - это

Either SomeException Char
Тебе понадобятся следующие функции:
eitherMap :: (a -> b) -> [Either e a] -> [Either e b]
eitherFoldl :: (b -> a -> b) -> b -> [Either e a] -> Either e b
eitherGroup ... 
ну и так далее, все потоковые операции, и, вуаля, у тебя поточная обработка Either значений ! При чем, все это без прерывания потока исполнения, компонуемо, вобщем, haskell-way.

s9gf4ult ★★ ()
Ответ на: комментарий от s9gf4ult

Ну во-первых, чтобы паттернматчить Either достаточно вычислить его только до конструктора

Да. И это потребует выбрать вход до конца. Ибо ошибка может произойти на последнем байте, и тогда вместо Left должен будет быть Right.

[Either e b]

Это не есть хорошо. Это намёк, что после ошибки выполнение может продолжиться.

Miguel ★★★★★ ()
Ответ на: комментарий от Miguel

Про Lazy IO я уже выше писал, кстати. Если я правильно понимаю, "функция, возвращающая строку, которая может гавкнуться" может быть только в IO.

dmfd ()
Последнее исправление: dmfd (всего исправлений: 1)
Ответ на: комментарий от dmfd

Если я правильно понимаю, «функция, возвращающая строку, которая может гавкнуться» может быть только в IO.

Это, собственно, почему?

Miguel ★★★★★ ()
Ответ на: комментарий от Miguel

проясни мысль про Source, у тебя же в случае кондуитов есть Source m o, где o это или String или

type DataPack a = Either ErrorType a

в первом случае ты будешь как обычно бросать исключение, во втором поведение с рассматриваемой точки зрения не отличается от [Either ErrorType a]. При этом если бы source мог возвращать значения и преждевременно закрывать весь downstream с своим значением, то разница бы была, а так он может только закрыть downstream и он уже сам что-то вернет.

А мысль, про (a, Maybe SomeException) интересна.

qnikst ★★★★★ ()
Ответ на: комментарий от dmfd

любая нетотальная функция хотя бы head (filter something), или map (5/) и то и другое чистое и может фейлиться, и то и другое можно обернуть в безопасный тип, но Miguel правильно отметил, что это приведет к той проблеме, что нужно будет вычислить всю функцию, чтобы понять не сломалась ли она.

qnikst ★★★★★ ()
Ответ на: комментарий от qnikst

При этом если бы source мог возвращать значения и преждевременно закрывать весь downstream с своим значением, то разница бы была, а так он может только закрыть downstream и он уже сам что-то вернет.

Ну дык Source-то может быть с любой монадой. Например, это может быть Source (EitherT SomeException IO) o. Если мы спарим его с Sink-ом, который умеет сворачивать вход, не загрязняя память, то, по идее, должны получить вменяемый результат (надо будет попробовать, кстати). Соответственно, так как ConduitM - инстанс MonadTrans, Source сможет закрыть downstream любым действием из EitherT SomeException IO () - например, Left чтототам.

Miguel ★★★★★ ()
Последнее исправление: Miguel (всего исправлений: 1)
Ответ на: комментарий от qnikst
module Test where
import Control.Monad (forM_)
import Control.Monad.Trans
import Control.Monad.Trans.Either
import Data.Conduit
import Data.Conduit.List (consume, fold)
megaSource :: Integer -> Source (EitherT Int IO) String
megaSource n = forM_ [1..n] $ \k -> if k >= n then lift (left 0) else yield (show n) -- шоп упало в самом конце
getOutput :: Monad m => Source m a -> m [a]
getOutput src = src $$ consume
isGood :: Integer -> IO Bool
isGood n = eitherT (const $ return False) (const $ return True) $ getOutput $ megaSource n
checkSum :: Monad m => Sink String m Int
checkSum = fold (\n str -> n + sum (map fromEnum str)) 0
sumConduit :: Integer -> IO (Maybe Int)
sumConduit n = eitherT (const $ return Nothing) (return . Just) $ megaSource n $$ checkSum

Наш Source выдаёт охрененной длины список строк в монаде EitherT Int IO (Int тут не в тему, можно было что угодно взять). Если мы заберём весь этот список и попытаемся его спаттерн-матчить (командой isGood $ 10^8), то ghci упадёт по нехватке памяти. Если же мы отправим его в Sink, который умеет сворачивать входной поток (командой sumConduit $ 10^8), то всё отработает, не расходуя слишком много памяти. Я для пробы запустил sumConduit $ 10^9 - работало охрененно долго, но выдало результат.

Miguel ★★★★★ ()
Ответ на: комментарий от qnikst

нужно будет вычислить всю функцию, чтобы понять не сломалась ли она.

Как я уже написал выше, это вовсе не обязательно. Если вычисление вида

vichislenie :: Either e a
То достаточно вычислить до слабой головной нормальной формы, чтобы понять, что перед тобой Left или Right. Во-вторых, Either также можно вычислять поточно, не всасывая весь поток вычислений с помощью поточных функций вида
eitherMap :: (a -> b) -> [Either e a] -> [Either e b]
eitherFoldl :: (b -> a -> b) -> b -> [Either e a] -> Either e b
eitherFilter :: (a -> Bool) -> [Either e a] -> [Either e a]

ну и так далее.

Для чистого кода это лучше, ибо позволяет детерминированно проводить вычисления без фейлов, и да, error - это примерно как undefined, согласен. Нужен чтобы либо помечать очевидные ошибки, требующие вмешательства программиста, либо для быстрого прототипирования. Чистый код бросать их не должен. Естественно есть исключения типа деления на ноль и head, которые просто не работают на некоторых данных, и программист должен это учитывать, то есть, если head выбрасывает ошибку, то это проблема программиста, а не пользователя.

s9gf4ult ★★ ()
Ответ на: комментарий от s9gf4ult

То достаточно вычислить до слабой головной нормальной формы, чтобы понять, что перед тобой Left или Right.

пусть foo :: String, bar :: String -> Either e a если мы делаем

case bar foo of
  Left .. -> ...
  Right .. -> ...
то вычисление bar во многих случаях должно обработать всё foo даже для того, чтобы вычислить WHNF, причем если это вычисление обламывается на последнем символе.

Про второе - да можно, а функции вида это mapM, filterM, fodlM над Either e.

про остальное - да.

qnikst ★★★★★ ()
Последнее исправление: qnikst (всего исправлений: 1)
Ответ на: комментарий от qnikst

Ты имеешь в виду, если a - рекурсинвый тип данных, типа списка или дерева, и может быть вычислен лишь частитчно ? Ну в этом случае операцию работающую с этим значением можно втянуть в само Either вычисление с помощью

eitherFoldl :: (b -> a -> b) -> b -> [Either e a] -> Either e b
только не над списком, а уже над твоим рекурсивным типом.

Согласен, что местами это не удобно, но, с другой стороны, это проставляет «правильную» строгость фейлящихся вычислений, как мне кажется.

s9gf4ult ★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.