Здравствуйте, 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.