Можно ли проверить возвращаемое значение функций ввода/вывода 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 будет достаточно умным, чтобы пропустить любые утверждения фальсификаций, которые он находит полностью, но фактические утверждения наверняка сбой вашей программы после сбоя.

Это очень маловероятно, так как ИМО - вам все равно придется проводить поведенческое тестирование в сложных сценариях, но я думаю, что это может помочь проверить правильность основных допущений, которые делает код.