Как уже отмечалось, нет чистый способ «имитировать» IO
, если вы уже используете вещи Лик e getLine
и putStrLn
. Вы должны изменить greeter
. Вы можете использовать версии hGetLine
и hPutStr
и выкрикнуть IO
с подделкой Handle
, или вы можете использовать метод Purify Code with Free Monads.
Это гораздо более сложный, но более общий и обычно подходит для такого рода насмешек, особенно когда он становится более сложным. Я кратко объясню это ниже, хотя детали несколько сложны.
Идея состоит в том, что вы будете создавать свою собственную «фальшивую IO» монаду, которая может быть «интерпретирована» несколькими способами. Первичная интерпретация заключается в том, чтобы использовать обычный IO
. Издевательская интерпретация заменяет getLine
некоторыми фальшивыми линиями и отгоняет все до stdout
.
Мы будем использовать пакет free
. Первый шаг - описать ваш интерфейс с помощью Functor
. Основное понятие состоит в том, что каждая команда является ветвью вашего типа данных-функтора и что «слот» функтора представляет собой «следующее действие».
{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Trans.Free
data FakeIOF next = PutStr String next
| GetLine (String -> next)
deriving Functor
Эти конструкторы почти как регулярные IO
функций с точки зрения кого-то строит FakeIOF
если вы игнорируете следующее действие. Если мы хотим PutStr
, мы должны предоставить String
.Если мы хотим GetLine
, мы предоставляем функцию, которая дает следующее действие при задании String
.
Теперь нам нужно немного запутать шаблон. Мы используем функцию liftF
, чтобы превратить наш функтор в монаду FreeT
. Обратите внимание, что мы предоставляем ()
в качестве следующего действия на PutStr
и id
в качестве нашей функции String -> next
. Оказывается, они дают нам правильные «возвращаемые значения», если мы подумаем о том, как будет вести себя наш FakeIO
Monad
.
-- Our FakeIO monad
type FakeIO a = FreeT FakeIOF IO a
fPutStr :: String -> FakeIO()
fPutStr s = liftF (PutStr s())
fGetLine :: FakeIO String
fGetLine = liftF (GetLine id)
Используя эти мы можем построить любой функциональности мы, как и переписать greeter
с минимальными изменениями.
fPutStrLn :: String -> FakeIO()
fPutStrLn s = fPutStr (s ++ "\n")
greeter :: FakeIO()
greeter = do
fPutStr "What's your name? "
name <- fGetLine
fPutStrLn $ "Hi, " ++ name
Это может выглядеть немного волшебный --- мы используем do
обозначения без определения Monad
экземпляра. Фокус в том, что FreeT f m
является Monad
для любых Monad
m
и Functor
f`.
Это завершает нашу функцию "maceded" greeter
. Теперь мы должны интерпретировать его так или иначе, поскольку мы практически не реализовали функциональность. Чтобы написать интерпретатор, мы используем функцию iterT
от Control.Monad.Trans.Free
. Это полностью общий тип выглядит следующим образом
iterT
:: (Monad m, Functor f) => (f (m a) -> m a) -> FreeT f m a -> m a
Но когда мы применяем его к нашему FakeIO
монады выглядит
iterT
:: (FakeIOF (IO a) -> IO a) -> FakeIO a -> IO a
, который намного лучше. Мы предоставляем ему функцию, которая принимает FakeIOF
функторов, заполненных IO
актонами в позиции «следующего действия» (как это получилось) до простого IO
действия и iterT
сделает магию превращения FakeIO
в реальную IO
.
Для нашего интерпретатора по умолчанию это очень просто.
interpretNormally :: FakeIO a -> IO a
interpretNormally = iterT go where
go (PutStr s next) = putStr s >> next -- next :: IO a
go (GetLine doNext) = getLine >>= doNext -- doNext :: String -> IO a
Но мы также можем сделать издевательский интерпретатор. Мы будем использовать средства IO
для хранения некоторого состояния, в частности циклической очереди поддельных ответов.
newQ :: [a] -> IO (IORef [a])
newQ = newIORef . cycle
popQ :: IORef [a] -> IO a
popQ ref = atomicModifyIORef ref (\(a:as) -> (as, a))
interpretMocked :: [String] -> FakeIO a -> IO a
interpretMocked greetings fakeIO = do
queue <- newQ greetings
iterT (go queue) fakeIO
where
go _ (PutStr s next) = putStr s >> next
go q (GetLine getNext) = do
greeting <- popQ q -- first we pop a fresh greeting
putStrLn greeting -- then we print it
getNext greeting -- finally we pass it to the next IO action
и теперь мы можем проверить эти функции
λ> interpretNormally greeter
What's your name? Joseph
Hi, Joseph.
λ> interpretMocked ["Jabberwocky", "Frumious"] (greeter >> greeter >> greeter)
What's your name?
Jabberwocky
Hi, Jabberwocky
What's your name?
Frumious
Hi, Frumious
What's your name?
Jabberwocky
Hi, Jabberwocky
Я не думаю, что это возможно без изменения 'greeter', к сожалению.'getLine' будет * всегда * читать из' stdin', который является типом пользователя. Самый простой способ изменить это - использовать вместо этого 'hGetLine', который позволяет указать, с какой' Handle' вы хотите получить линию. Возможно также (я очень не уверен в этом), чтобы сделать некоторые манипуляции на низком уровне и настроить перенаправление себя с помощью модуля 'GHC.IO.Handle', но это, безусловно, последнее, что вы хотите сделать. – kqr
Я не знаю, возможно ли это (сначала я попытался найти способ записи в 'stdin', но это не позволяет вам), b ut это не то, для чего предназначена библиотека' pipe'. 'pipe' предназначен для написания программы, в которой вы отделяете генераторы данных от шагов обработки данных. Вы можете использовать его с IO (и это часто бывает), я бы рекомендовал [tutorial] (http://hackage.haskell.org/package/pipes-4.0.0/docs/Pipes-Tutorial.html) – bheklilr
Извините, Я не понимаю, что разумно в этой идее. Для меня это звучит просто как ужасный хак. Если аргумент 'IO()' должен иметь дело с данными из других функций Haskell, то почему он не принимает его как параметр? 'IOwithinputs :: [String] -> ([String] -> IO()) -> IO()'. (Обратите внимание, что тогда, в основном, 'IOwithinputs ≡ flip ($)'.) – leftaroundabout