Haskell является чистым функциональным языком, что означает, что функции Haskell не имеют никакого влияния стороны. Ввод-вывод реализован с помощью монад, которые представляют блоки вычисления ввода-вывода.
Действительно ли возможно протестировать возвращаемое значение функций ввода-вывода Haskell?
Скажем, у нас есть простое 'привет мировая' программа:
main :: IO ()
main = putStr "Hello world!"
Для меня действительно ли возможно создать тестовую обвязку, которая может работать main
и проверьте, что монада ввода-вывода это возвращает корректное 'значение'? Или то, что монады, как предполагается, являются непрозрачными блоками вычисления, препятствуют тому, чтобы я делал это?
Отметьте, я не пытаюсь сравнить возвращаемые значения действий ввода-вывода. Я хочу сравнить возвращаемое значение функций ввода-вывода - сама монада ввода-вывода.
С тех пор во вводе-выводе Haskell возвращается, а не выполняется, я надеялся исследовать блок вычисления ввода-вывода, возвращенного вводом-выводом, функционируют и видят, было ли это корректно. Я думал, что это могло позволить функциям ввода-вывода быть единицей, протестированной способом, они не могут на императивных языках, где ввод-вывод является побочным эффектом.
Я бы сделал это, создав мою собственную монаду ввода-вывода, содержащую действия, которые я хотел смоделировать. Я бы запустил монадические вычисления, которые я хочу сравнить в своей монаде, и сравнить эффекты, которые они имели.
Давайте рассмотрим пример. Предположим, я хочу моделировать полиграфический материал. Затем я могу смоделировать свою монаду ввода-вывода следующим образом:
data IO a where
Return :: a -> IO a
Bind :: IO a -> (a -> IO b) -> IO b
PutChar :: Char -> IO ()
instance Monad IO where
return a = Return a
Return a >>= f = f a
Bind m k >>= f = Bind m (k >=> f)
PutChar c >>= f = Bind (PutChar c) f
putChar c = PutChar c
runIO :: IO a -> (a,String)
runIO (Return a) = (a,"")
runIO (Bind m f) = (b,s1++s2)
where (a,s1) = runIO m
(b,s2) = runIO (f a)
runIO (PutChar c) = ((),[c])
Вот как я могу сравнить эффекты:
compareIO :: IO a -> IO b -> Bool
compareIO ioA ioB = outA == outB
where ioA = runIO ioA ioB
Есть вещи, которые эта модель не обрабатывает. Например, вводить сложно. Но я надеюсь, что он подойдет для вашего использования. Я также должен упомянуть, что есть более умные и эффективные способы моделирования эффектов таким образом. Я выбрал именно этот путь, потому что считаю его наиболее простым для понимания.
Для получения дополнительной информации я могу порекомендовать статью «Красавица в чудовище:
Сожалею, что вы не можете этого сделать.
unsafePerformIO
в основном позволяет вам это сделать. Но я бы очень предпочел, чтобы вы не использовали его.
Foreign.unsafePerformIO :: IO a -> a
: /
Мне нравится этот ответ на похожий вопрос по SO и комментарии к нему. В основном, IO, как правило, приводит к некоторым изменениям, которые могут быть замечены со стороны; ваше тестирование должно быть связано с тем, кажется ли это изменение правильным. (Например, была произведена правильная структура каталога и т.д.)
В основном, это означает "тестирование поведения", которое в сложных случаях может быть довольно болезненным. Это часть причины, по которой следует свести к минимуму часть кода, специфичную для IO, и перенести как можно больше логики в чистые (следовательно, супер легко тестируемые) функции.
И опять же, можно использовать функцию assert:
actual_assert :: String -> Bool -> IO ()
actual_assert _ True = return ()
actual_assert msg False = error $ "failed assertion: " ++ msg
faux_assert :: String -> Bool -> IO ()
faux_assert _ _ = return ()
assert = if debug_on then actual_assert else faux_assert
(Возможно, вы захотите определить debug_on
в отдельном модуле, построенном непосредственно перед сборкой с помощью скрипта сборки. Также, это, скорее всего, будет предоставлено в более отшлифованном виде пакетом на Hackage, если не стандартной библиотекой.... Если кто-то знает о таком инструменте, пожалуйста, отредактируйте этот пост/комментарий, чтобы я мог его отредактировать.)
I думаю GHC будет достаточно умён, чтобы полностью пропустить любые фальшивые утверждения, которые он найдёт, если реальные утверждения определённо приведут к сбою вашей программы.
Этого, IMO, вряд ли будет достаточно - вам всё равно придётся проводить тестирование поведения в сложных сценариях - но я думаю, что это может помочь проверить, что основные предположения, которые делает код, верны.
. Вы можете проверить некоторый монадный код с помощью QuickCheck 2 . Прошло много времени с тех пор, как я прочитал статью, поэтому я не помню, применимо ли это к операциям ввода-вывода или к каким типам монадских вычислений оно может быть применено. Также, возможно, вам будет сложно выразить свои юнит-тесты в свойствах QuickCheck. Тем не менее, как очень довольный пользователь QuickCheck, я скажу, что это lot лучше, чем ничего не делать или чем взламывать с помощью небезопасногоPerformIO
.
Внутри IO monad можно проверить возвращаемые значения функций ввода-вывода. Проверять возвращаемые значения вне IO monad небезопасно: это означает, что это можно сделать, но только с риском взлома вашей программы. Только для экспертов.
Стоит отметить, что в показанном вами примере значение main
имеет тип IO()
, что означает "Я - IO действие, которое при выполнении которого выполняет некоторое количество входов/выходов, а затем возвращает значение типа ()
". Тип ()
произносится как "единица", и существует только два значения этого типа: пустой кортеж (также написан ()
и произносится как "единица") и "дно", которое является именем Хаскелла для вычисления, которое не завершается или иным образом не завершается.
Следует отметить, что тестирование возвратных значений функций ввода-вывода из в пределах IO monad совершенно просто и нормально, и что идиоматический способ сделать это - использовать нотацию do
.