我正在使用Netwire
haskell库进行测试,并使用简单的time
线进行测试:
import Control.Wire
import Prelude hiding ((.), id)
import Control.Monad.IO.Class
import Data.Functor.Identity
import System.IO
wire :: (HasTime t s) => Wire s () m a t
wire = time
run :: (HasTime t s, MonadIO m, Show b, Show e) =>
Session m s -> Wire s e m a b -> m ()
run session wire = do
(dt, session') <- stepSession session
(wt', wire') <- stepWire wire dt $ Right undefined
case wt' of
-- | Exit
Left _ -> return ()
Right x -> do
liftIO $ do
putChar '\r'
putStr $ either (\ex -> show ex) show wt'
hFlush stdout
-- Interactivity here?
gotInput <- hReady stdin
if gotInput then
return ()
else return ()
run session' wire'
main :: IO ()
-- main = testWire clockSession_ wire
main = run clockSession_ wire
注意:run
基本上是从testWire
修改的,所以我不知道这是否是形成电线网络的正确方法。部分代码来自http://todayincode.tumblr.com/post/96914679355/almost-a-netwire-5-tutorial,但该教程没有说明事件。
现在我正在尝试为程序添加一点交互性。现在,在按任何键时退出程序。我想我应该做一些事件切换。但是,我被困在这里因为我找不到改变wire'
或切换行为的方法。我试图阅读API文档和源代码,但我没有看到如何实际“触发”事件或使用它来切换电线。
同样,由于我还不熟悉Haskell,我可能在这里犯了一些大愚蠢的错误。
更新1/2
我通过以下代码实现了目标。任何按键时计时器停止。 更新2 我设法将pollInput
分成另一个IO
功能,Yay!
import Control.Wire
import Prelude hiding ((.), id)
import Control.Monad.IO.Class
import Data.Functor.Identity
import System.IO
wire :: (HasTime t s) => Wire s () m a t
wire = time
run :: (HasTime t s, MonadIO m, Show b, Show e) =>
Session m s -> Wire s e m a b -> m ()
run session wire = do
-- Get input here
input <- liftIO $ pollInput
(dt, session') <- stepSession session
(wt', wire') <- stepWire wire dt $ input
case wt' of
-- | Exit
Left _ -> liftIO (putStrLn "") >> return ()
Right x -> do
liftIO $ do
putChar '\r'
putStr $ either (\ex -> show ex) show wt'
hFlush stdout
run session' wire'
pollInput :: IO (Either a b)
pollInput = do
gotInput <- hReady stdin
if gotInput then
return (Left undefined)
else return (Right undefined)
setup :: IO ()
setup = do
hSetBuffering stdin NoBuffering
hSetBuffering stdout NoBuffering
main :: IO ()
main = do
setup
run clockSession_ wire
然而,这引发了一些进一步的问题。首先,这是一个好习惯吗? 第二,pollInput
的类型是什么?我试图手动输入但没有成功。但是,自动型扣除工作。 德尔>
这是我对此代码如何工作的解释:
首先,轮询来自控制台的用户输入,并且在某些逻辑之后,生成线路的“输入”(名称选择不佳,但生成的输入是线路输入)并沿网络传递。在这里,我只是传递一个抑制(Left something
),并将导致循环退出。当然,退出时,程序会生成一个换行符,使控制台看起来更好。
(好吧,我仍然不明白Event
如何运作)
更新3/4
在阅读了@Cirdec的回答并在我的编辑器上摆弄了很多东西之后,我得到了这个没有IORef
的单线程版本,也在按下'x' Update 4 时退出:< del>(但它不输出任何东西):
import Control.Wire
import Prelude hiding ((.),id)
import Control.Wire.Unsafe.Event
import System.IO
import Control.Monad.IO.Class
data InputEvent = KeyPressed Char
| NoKeyPressed
deriving (Ord, Eq, Read, Show)
type OutputEvent = IO ()
--- Wires
example :: (HasTime t s, Monad m, Show t) =>
Wire s () m (Event [InputEvent]) (Event [OutputEvent])
example = switch $
(fmap ((:[]) . print) <$> periodic 1 . time
&&&
fmap (const mkEmpty) <$> filterE (any (== KeyPressed 'x'))
)
readKeyboard :: IO (Either e (InputEvent))
readKeyboard = do
hSetBuffering stdin NoBuffering
gotInput <- hReady stdin
if gotInput then do
c <- getChar
return $ Right $ KeyPressed c
else return $ Right $ NoKeyPressed
output :: [OutputEvent] -> IO ()
output (x:xs) = id x >> output xs
output _ = return ()
run :: (HasTime t s, MonadIO m) =>
Session m s -> Wire s e m (Event [InputEvent]) (Event [OutputEvent]) -> m e
run = go
where
go session wire = do
-- | inputEvent :: Event InputEvent
inputEvent <- liftIO $ readKeyboard
(dt, session') <- stepSession session
(wt', wire') <- stepWire wire dt (Event <$> (fmap (:[]) inputEvent))
-- (wt', wire') <- stepWire wire dt (Right undefined)
case wt' of
Left a -> return a
Right bEvent -> do
case bEvent of
Event b -> liftIO $ output b
_ -> return ()
go session' wire'
main = do
run clockSession_ example
我认为这比我原来的要好得多,但我仍然不完全相信这是否是好的做法。
答案 0 :(得分:3)
如果您不想阻止输入和输出,请不要阻止输入和输出。为了演示如何将netwire连接到事件,我们将为运行连线做一个小框架。我们通过在单独的线程中执行所有IO
来避免阻止线路的步进。
如果我们正在开发框架,我们可以从netwire documentation解构Event
。
默认情况下,Netwire不会导出
Event
类型的构造函数。如果您是框架开发人员,则可以导入Control.Wire.Unsafe.Event
模块以实现自己的事件。
这让我们看到Event
只是
data Event a = NoEvent | Event a
我们将创建一个非常简单的框架,在m
中使用一个操作进行输入,一个用于输出。它运行一个动作m (Either e a)
来读取动作或禁止动作。当电线禁止时,它会运行一个动作b -> m ()
来输出或停止。
import Control.Wire
import Prelude hiding ((.), id)
import Control.Wire.Unsafe.Event
run :: (HasTime t s, Monad m) =>
m (Either e a) -> (b -> m ()) ->
Session m s -> Wire s e m (Event a) (Event b) -> m e
run read write = go
where
go session wire = do
(dt, session') <- stepSession session
a <- read
(wt', wire') <- stepWire wire dt (Event <$> a)
case wt' of
Left e -> return e
Right bEvent -> do
case bEvent of
Event b -> write b
_ -> return ()
go session' wire'
我们将使用它运行一个示例程序,每秒输出一次时间,并在按下'x'
键时停止(禁止)。
example :: (HasTime t s, Monad m, Show t) =>
Wire s () m (Event [InputEvent]) (Event [OutputEvent])
example = switch $
(fmap ((:[]) . print) <$> periodic 1 . time)
&&&
(fmap (const mkEmpty) <$> filterE (any (== KeyPressed 'x')))
如果在同一时间步骤中发生多个事件,则输入和输出事件会携带多个事件。输入事件只是按下的字符键。输出事件为IO
个动作。
data InputEvent = KeyPressed Char
deriving (Ord, Eq, Read, Show)
type OutputEvent = IO ()
我们的非阻塞IO将运行三个线程:输入线程,输出线程和线程线程。他们将通过原子修改IORef
来相互沟通。这对于一个示例程序来说是过度的(我们可能在阅读时使用了hReady
而对于生产程序来说还不够(IO线程会在字符和输出上等待)。在实践中,事件和调度输出的轮询通常由其他一些IO框架(OpenGL,一个gui工具包,一个游戏引擎等)提供。
import Data.IORef
type IOQueue a = IORef [a]
newIOQueue :: IO (IOQueue a)
newIOQueue = newIORef []
readIOQueue :: IOQueue a -> IO [a]
readIOQueue = flip atomicModifyIORef (\xs -> ([], reverse xs))
appendIOQueue :: IOQueue a -> [a] -> IO ()
appendIOQueue que new = atomicModifyIORef que (\xs -> (reverse new ++ xs, ()))
主线程设置队列,生成IO线程,运行连线,并在程序停止时向IO线程发出信号。
import Control.Concurrent.MVar
import Control.Concurrent.Async
import Control.Monad.IO.Class
runKeyboard :: (HasTime t s, MonadIO m) =>
Session m s -> Wire s e m (Event [InputEvent]) (Event [OutputEvent]) -> m e
runKeyboard session wire = do
stopped <- liftIO newEmptyMVar
let continue = isEmptyMVar stopped
inputEvents <- liftIO newIOQueue
outputEvents <- liftIO newIOQueue
inputThread <- liftIO $ async (readKeyboard continue (appendIOQueue inputEvents . (:[])))
outputThread <- liftIO $ async (runEvents continue (sequence_ <$> readIOQueue outputEvents))
let read = liftIO $ Right <$> readIOQueue inputEvents
let write = liftIO . appendIOQueue outputEvents
e <- run read write session wire
liftIO $ putMVar stopped ()
liftIO $ wait inputThread
liftIO $ wait outputThread
return e
输入线程等待键,当没有输入就绪时旋转。它将KeyPressed
个事件发送到队列。
import System.IO
readKeyboard :: IO Bool -> (InputEvent -> IO ()) -> IO ()
readKeyboard continue send = do
hSetBuffering stdin NoBuffering
while continue $ do
ifM (hReady stdin) $ do
a <- getChar
send (KeyPressed a)
ifM :: Monad m => m Bool -> m a -> m ()
ifM check act = do
continue <- check
if continue then act >> return () else return ()
while :: Monad m => m Bool -> m a -> m ()
while continue act = go
where
go = ifM continue loop
loop = act >> go
输出线程运行它发送的动作,只要它被指示继续(并且在发出信号停止以确保所有输出都发生之后再一次)。
runEvents :: IO Bool -> (IO (IO ())) -> IO ()
runEvents continue fetch = (while continue $ fetch >>= id) >> fetch >>= id
我们可以使用runKeyboard
运行示例程序。
main = runKeyboard clockSession_ example
答案 1 :(得分:0)
首先,我要指向Kleisli Arrow in Netwire 5?。在尝试了解Monads和Arrows的漫长时间之后,我想出了那个答案。 我将尽快使用Kleisli Wire的最小例子。
此程序仅回显用户键入的内容,并在遇到q
时退出。虽然没用,但它证明了使用Netwire 5可能是一种很好的做法。
mkKleisli :: (Monad m, Monoid e) => (a -> m b) -> Wire s e m a b
mkKleisli f = mkGen_ $ \a -> liftM Right $ f a
这是在引用的帖子中的答案中写的Kleisli线构造函数。总之,此函数将任何Kleisli函数a -> m b
提升为Wire s e m a b
。这是我们在该计划中所做的任何I / O的核心。
由于我们作为用户类型回应,hGetChar
可能是最佳选择。因此,我们将其提升为电线。
inputWire :: Wire s () IO () Char
inputWire = mkKleisli $ \_ -> hGetChar stdin
同样,我们使用以下电汇在屏幕上输出字符。
outputWire :: Wire s () IO Char ()
outputWire = mkKleisli $ putChar
然后,为了确定何时需要退出,构造纯线以在输入True
时输出q
(请注意,可以使用mkSF_
代替arr
})。
quitWire :: (Monad m, Monoid e) => Wire s e m Char Bool
quitWire = arr $ quitNow
where
quitNow c
| c == 'q' || c == 'Q' = True
| otherwise = False
要实际使用quitting的信息,我们需要编写一个特殊的(但非常简单的)runWire
函数,该函数运行类型为Wire s e m () Bool
的连线。当线被禁止或返回false时,功能结束。
runWire :: (Monad m) => Session m s -> Wire s e m () Bool -> m ()
runWire s w = do
(ds, s') <- stepSession s
(quitNow, w') <- stepWire w ds (Right ())
case quitNow of
Right False -> runWire s' w'
_ -> return ()
现在,让我们把电线放在一起。
mainWire = inputWire >>> (quitWire &&& outputWire) >>> arr (\(q,_) -> q)
当然我们可以使用Arrow语法:
mainWire = proc _ -> do
c <- inputWire -< ()
q <- quitWire -< c
outputWire -< c
returnA -< q
不确定proc
版本是否更快,但在这个简单的示例中,两者都非常易读。
我们从inputWire
获取输入,将其提供给quitWire
和outputWire
并获取元组(Bool, ())
。然后我们将第一个作为最终输出。
最后,我们在main
中运行所有内容!
main = do
hSetEcho stdin False
hSetBuffering stdin NoBuffering
hSetBuffering stdout NoBuffering
runWire clockSession_ mainWire
这是我使用的最终代码:
{-# LANGUAGE Arrows #-}
module Main where
import Control.Wire
import Control.Monad
import Control.Arrow
import System.IO
import Prelude hiding ((.), id)
mkKleisli :: (Monad m, Monoid e) => (a -> m b) -> Wire s e m a b
mkKleisli f = mkGen_ $ \a -> liftM Right $ f a
inputWire :: Wire s () IO () Char
inputWire = mkKleisli $ \_ -> hGetChar stdin
outputWire :: Wire s () IO Char ()
outputWire = mkKleisli $ putChar
quitWire :: (Monad m, Monoid e) => Wire s e m Char Bool
quitWire = arr $ quitNow
where
quitNow c
| c == 'q' || c == 'Q' = True
| otherwise = False
runWire :: (Monad m) => Session m s -> Wire s e m () Bool -> m ()
runWire s w = do
(ds, s') <- stepSession s
(quitNow, w') <- stepWire w ds (Right ())
case quitNow of
Right False -> runWire s' w'
_ -> return ()
mainWire = inputWire >>> (quitWire &&& outputWire) >>> arr (\(q,_) -> q)
main = do
hSetEcho stdin False
hSetBuffering stdin NoBuffering
hSetBuffering stdout NoBuffering
runWire clockSession_ mainWire