Можно ли проверить возвращаемое значение функций ввода/вывода Haskell?
Haskell - это чистый функциональный язык, что означает, что функции Haskell не имеют побочных эффектов. I/O реализуется с использованием монад, представляющих куски вычислений ввода-вывода.
Можно ли проверить возвращаемое значение функций ввода/вывода Haskell?
Скажем, у нас есть простая программа "hello world":
main :: IO ()
main = putStr "Hello world!"
Возможно ли создать тестовый жгут, который может запускать main
, и проверить, чтобы монада ввода/вывода вернула правильное значение? Или тот факт, что монады должны быть непрозрачными блоками вычислений, мешает мне это делать?
Заметьте, я не пытаюсь сравнивать возвращаемые значения операций ввода-вывода. Я хочу сравнить возвращаемое значение функций ввода-вывода - сама монада ввода-вывода.
Так как в Haskell I/O возвращается, а не выполняется, я надеялся изучить кусок вычисления ввода-вывода, возвращаемый функцией ввода-вывода, и посмотреть, правильно ли оно было. Я думал, что это может позволить тестировать модули ввода/вывода таким образом, чтобы они не были в императивных языках, где I/O является побочным эффектом.
Ответы
Ответ 1
То, как я это сделаю, - это создать мою собственную монаду IO, в которой содержатся действия, которые я хотел бы моделировать. Я бы выполнил монадические вычисления, которые я хочу сравнить в своей монаде, и сравнить эффекты, которые у них были.
Возьмем пример. Предположим, я хочу моделировать печатные материалы. Затем я смогу смоделировать мою монашку IO следующим образом:
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
Есть вещи, которые эта модель не обрабатывает. Вход, например, сложный. Но я надеюсь, что это подойдет вам. Следует также упомянуть, что есть более умные и эффективные способы моделирования эффектов таким образом. Я выбрал именно этот путь, потому что я считаю, что это самый простой способ понять.
Для получения дополнительной информации я могу порекомендовать статью "Красота в зверю: функциональная семантика для неуклюжий отряд", которую можно найти на этой странице наряду с некоторыми другими соответствующими документами.
Ответ 2
В монаде IO вы можете проверить возвращаемые значения функций ввода-вывода. Чтобы проверить возвращаемые значения вне монеты ввода-вывода, это небезопасно: это означает, что это можно сделать, но только под угрозой взлома вашей программы. Только для экспертов.
Стоит отметить, что в примере, который вы показываете, значение main
имеет тип IO ()
, что означает: "Я - это действие IO, которое при выполнении делает некоторые операции ввода-вывода, а затем возвращает значение тип ()
." Тип ()
произносится как "единица", и есть только два значения этого типа: пустой кортеж (также записанный ()
и произносится "единица" ) и "нижний", что является именем Haskell для вычисления, которое не прекратить или иным образом пойти не так.
Стоит отметить, что тестирование возвращаемых значений IO-функций из IO-монады совершенно легко и нормально, и что идиоматический способ сделать это - использовать нотацию do
.
Ответ 3
Вы можете протестировать некоторый монадический код с помощью QuickCheck 2. Прошло много времени с тех пор, как я прочитал эту статью, поэтому не помню, относится ли она к действиям IO или к каким монадическим вычислениям она может быть применена. Кроме того, возможно, вам сложно выразить ваши модульные тесты как свойства QuickCheck. Тем не менее, как очень довольный пользователь QuickCheck, я скажу это намного лучше, чем ничего не делать, или взломать с помощью unsafePerformIO
.
Ответ 4
Мне жаль говорить вам, что вы не можете этого сделать.
unsafePerformIO
в основном позволяет вам выполнить это. Но я бы предпочел, чтобы вы использовали не.
Foreign.unsafePerformIO :: IO a -> a
:/
Ответ 5
Мне нравится этот ответ на аналогичный вопрос о 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
в отдельном модуле, построенном непосредственно перед сборкой с помощью сборки script. Кроме того, это очень вероятно, будет предоставлено в более отполированной форме пакетом на Hackage, если не стандартная библиотека... Если кто-то знает о таком инструменте, отредактируйте этот пост/комментарий, чтобы я мог редактировать.)
Я думаю, что GHC будет достаточно умным, чтобы пропустить любые утверждения фальсификаций, которые он находит полностью, но фактические утверждения наверняка сбой вашей программы после сбоя.
Это очень маловероятно, так как ИМО - вам все равно придется проводить поведенческое тестирование в сложных сценариях, но я думаю, что это может помочь проверить правильность основных допущений, которые делает код.