Ответ 1
Наиболее практичная причина этого - тестируемость и более точные сигнатуры типов.
Ключевой силой haskell является то, насколько хорошо вы можете указать, что делает функция через систему типов. Сравните тип С#/java:
public int CSharpFunction(int param) { ...
с haskell one:
someFunction :: Int -> Int
The haskell не только сообщает нам типы, необходимые для параметров и типа возврата, но также и то, что может повлиять на функцию. Например, он не может выполнять никаких операций ввода-вывода и не может читать или изменять какое-либо глобальное состояние или получать доступ к статическим данным конфигурации. Для функции С# также не может быть прав, но мы не можем сказать.
Это отличная помощь при тестировании. Мы знаем, что единственное, что нам нужно проверить с помощью haskell someFunction
, - это получить ожидаемые выходные данные для некоторых выборочных входов. Никакой возможной установки не требуется, и функция никогда не даст другого результата для одного и того же ввода. Это делает тестирование довольно простым с чистыми функциями.
Однако часто функция не может быть чистой. Например, для чтения может потребоваться доступ к некоторой глобальной информации. Мы могли бы просто добавить еще один параметр к функции:
readerFunc :: GlobalConfig -> Int -> Int
Но часто бывает проще использовать монаду, так как они легче сочинять:
readerFunc2 :: Int -> Reader GlobalConfig Int
Тестирование это почти так же просто, как тестирование чистой функции. Нам просто нужно проверить различные перестановки входного значения Int и значение конфигурации считывателя GlobalConfig.
Возможно, функция должна выписать значения (например, для ведения журнала). Это также можно сделать с помощью монады:
writerFunc :: Int -> Writer String Int
Опять тестирование это почти так же просто, как для чистой функции. Мы просто проверяем, если для данного ввода Int
возвращается соответствующий Int
, а также правый конечный писатель String
.
Другая функция, возможно, потребуется прочитать и изменить состояние:
stateFunc :: Int -> State GlobalState Int
Это сложнее проверить. Мы не только должны проверять вывод, используя различные входные данные Ints и начальные GlobalStates, но нам также нужно проверить, является ли конечное значение GlobalState правильным.
Теперь рассмотрим функцию, которая:
- Требуется прочитать из типа данных ProgramConfig
- Запись значений в строку для ведения журнала
- Использовать и изменять значение
ProgramState
.
Мы могли бы сделать что-то вроде этого:
data ProgramData = ProgramData { pState :: ProgramState, pConfig :: ProgramConfig, pLogs :: String }
complexFunction :: Int -> State ProgramData Int
Однако этот тип не очень точен. Это означает, что ProgramConfig может быть изменен, чего не будет. Это также означает, что функция может использовать значение pLogs, чего не будет. Кроме того, тестирование является более сложным, поскольку теоретически функция может случайно изменить конфигурацию программы или использовать текущие значения pLogs в своих вычислениях.
Это может быть значительно улучшено с помощью этого:
betterFunction :: Int -> RWS ProgramConfig String ProgramState Int
Этот тип очень точный. Мы знаем, что ProgramConfig только читается. Мы знаем, что String
только изменилось. Единственная часть, которая требует как чтения, так и записи, - это ProgramState
, как и ожидалось. Это проще проверить, так как нам нужно только проверить различные комбинации ProgramConfig, ProgramState и Int
для ввода и проверить выходные данные Int
, String
и ProgramState
для вывода. Мы знаем, что мы не можем случайно изменить конфигурацию программы или использовать текущее значение журнала программы в наших вычислениях.
Система типа haskell должна помочь нам, мы должны предоставить ей столько информации, сколько можем, чтобы она могла ловить ошибки до того, как мы это сделаем.
(Обратите внимание, что каждый тип haskell в этом ответе может быть эквивалентным типу С# вверху, в зависимости от того, как фактически реализована функция С#).