我有一个这样的程序,
start :: [Q] -> R -> IO R
start qs = fix $ \recurse r -> do
q <- select qs
(r', exit) <- askQ q r
(if exit
then return
else recurse) r'
需要一个
Q
问题列表、一个 R
eport,并在 R
monad 中返回一个新的 IO
eport,因为 select
需要它随机选择一个问题(也因为 askQ
将等待用户键盘输入);但是,用户在执行 exit
时没有选择 askQ
,start
会递归调用自身。 (fix $ \recurse
是编写递归 lambda 的技巧。)
上面的代码听起来很像一些东西:
State
monad,或者更恰当地说,StateT
monad 转换器,因为 R
在 start
的递归过程中不断演化;forever
应用组合器,因为start
是递归的,如果用户愿意的话(即,如果他们从不要求退出),它可能会永远运行;MaybeT
单子变压器,因为Maybe
实现了MonadPlus
,应该能够短路forever
,基于我在forever
实现之前读到的内容。
type M = MaybeT (StateT R IO)
start :: [Q] -> M ()
start qs = void $ many (select qs >>= askQ)
这假设 select
和
askQ
将被重写以在
M
monad 而不是
IO
中运行:
select :: [Q] -> M Q
askQ :: Q -> M ()
结果非常...简洁。一个独立的例子,供后代使用...
import Data.Coerce
import Control.Applicative
import Control.Monad
import Control.Monad.State
import Control.Monad.Trans.Maybe
import System.Random
newtype Q = Q String deriving (Show)
newtype R = R [String] deriving (Show)
type M = MaybeT (StateT R IO)
runM :: M a -> IO (Maybe a, R)
runM = flip runStateT (R []) . runMaybeT
select :: [Q] -> M Q
select qs = (qs !!) <$> randomRIO (0, length qs - 1)
askQ :: Q -> M ()
askQ (Q q) = do
liftIO $ putStrLn q
r <- liftIO getLine
if r == "exit" then mzero
else modify (coerce (r:))
start :: [Q] -> M ()
start qs = void $ many (select qs >>= askQ)
main :: IO ()
main = do
result <- runM $ start [ Q "Is this idiomatic?"
, Q "Seriously, what's wrong with recursion?"
]
print result
似乎有效:
λ> main
Seriously, what's wrong with recursion?
nothing
Is this idiomatic?
yes
Is this idiomatic?
exit
(Just (),R ["yes","nothing"])