Re[14]: Result objects - все-таки победили Exceptions?
От: maxkar  
Дата: 10.01.25 20:06
Оценка: 5 (2) +1
Здравствуйте, Sinclair, Вы писали:

S>Читаем дальше:

S>

S>Haskell solves the problem a diplomatic way: Functions return error codes, but the handling of error codes does not uglify the calling code.

S>Там написана неправда?

Там нет примеров более сложной (бизнес-)логики, которая использует хотя бы три-четыре библиотеки от разных разработчиков. В статье идеальный пример, когда все "исключения" могут быть определены в виде одного алгебраического типа. Что хорошо работает в примере и очень плохо работает в реальной жизни. Например, тот же

data ApplicationException =
      ReadException
    | WriteException

instance ThrowsRead ApplicationException where
    throwRead = ReadException

instance ThrowsWrite ApplicationException where
    throwWrite = WriteException


Является аналогом

final String content;
try {
  content = readFile(...);
} catch (IOException e) {
  throw new ReadException();
}

final String content;
try {
  writeFile(...);
} catch (IOException e) {
  throw new WriteException();
}


Информация о том, почему "не смогла", потеряна. Ладно, можно добавить исключение в ApplicationException:

data ApplicationException =
      ReadException IOException
    | WriteException IOException


Уже лучше. Хотя... Почему у нас WriteProtected является возможной ошибкой для read? Давайте честно:

data InputException =
     FileDoesNotExist
   | ReadProtected
   deriving (Show, Eq, Enum)

data OutputException =
     DiskFull
   | FileCountNotBeCreated
   | WriteProtected
   | NoSpaceOnDevice
   deriving (Show, Eq, Enum)   deriving (Show, Eq, Enum)

data ApplicationException =
      ReadException InputException
    | WriteException OutputException


Все хорошо? Почти. Теперь у нас усложняется copyFile. Начальное определение ниже уже не работает

copyFile src dst =
    writeFile dst =<< readFile src

В первом случае у нас InputException, во втором — OutputException. И нужно их к общему типу приводить. Ну да, есть дальше примеры с ReadException/NoReadException. Только там вроде бы все комбинации исключений (ThrowsWrite (ReadException e) и подобные) определяются. Это хорошо для примера. А что мы будем делать в реальном коде, когда исключений может быть больше? Всякие SQLException, MongoException, RedisException. При этом все эти SQL/Mongo/Redis тоже не просто алгебраические типы, а такие же тайпклассы со сложной структурой. Можно применять примеры из статьи, но это же куча ручного кода! Удобства там мало. А уж если мы будем добавлять полиморфизм...

Давайте сделаем теперь consumeFile, которая получает функцию для обработки содержимого файла:

consumeFile src consumer =
    consumer =<< readFile src
copyFile src dst = consumeFile src (writeFile dst)

Что мы будем делать в этом случае? И выйдут ли удобочитаемые сигнатуры? И что будет, если у нас там бизнес-логика (те же хранилища, часть из которых — SQL, а другая — Mongo). Можно оставить вывод типов компилятору. Только вот какие будут сообщения об ошибках, если типы не сходятся? Я не уверен, что это можно будет удобно читать.

Ладно, все выше — это прелюдия. Суровая реальность состоит в том, что Haskell — ленивый язык. Вот возьмем какой-нибудь Prelude.readFile. Давайте пока не будем смотреть на то, что она возвращает банальный IO вместо логичного ExceptionalT ReadException IO (). Давайте посмотрим на результат. А в результате у нас ленивая строка, которая читается при необходимости. Теперь возьмем пример take 5 <$> readFile "/dev/zero". Вопрос — где у нас будет исключение, если вдруг произошла ошибка диска? Произошло ли у нас исключение в take 5? Или только в IO, которое получилось после <$>? Первый вариант потребует много переделок. List будет таскать список ошибок, которые могут возникать при доступе. Все функции работы со списком будут параметризованы этим типом исключений. Второй вариант (исключения только в IO) будет плохо работать в более сложных случаях:

applyTemplate :: String => String => Exceptional TemplateApplicationIssue String
applyTemplate template data =
  do
    parsedTemplate <- liftTemplateErrors(parseTemplate template)
    parsedData <- liftDataErrors(parseData data)
    return (applyTemplateToData parsedTemplate parsedData)


applyFileTemplate templateFile dataFile =
  do
    template <- readFile templateFile
    data <- readFile dataFile
    return (applyTemplate template data)

parseTemplate :: String => Exceptional TemplateIssue Template
parseData :: String => Exceptional DataIssue Data
liftTemplateErrors :: Exceptional TemplateIssue Template => Exceptional TemplateApplicationIssue Template
liftDataErrors :: Exceptional DataIssue Template => Exceptional TemplateApplicationIssue Template

Мы можем отличить ошибки шаблона от ошибок данных. Ну, почти. По нашей логике мы не можем отличить ошибки чтения шаблона и чтения данных. Они же происходят в результирующем IO (результате applyFileTemplate) а не в applyTemplate! Как-то это не очень удобно в обработках. Как мы теперь будем понимать, что не прочиталось?

В общем, в Haskell ситуация с ошибками примерно та же, что и в Java, и в C#, и в большинстве остальных языков с исключениями. Коды возврата удобны для случаев, когда ошибка ожидаемая, частая и обрабатывается обычно по месту вызова. А в остальных случаях начинаются пляски и типами и конвертацией. А все "более сложные" случаи заметаются под ковер https://downloads.haskell.org/~ghc/6.10.1/docs/html/libraries/base/System-IO-Error.html.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.