4

私はNetwirehaskell ライブラリでテストしており、単純な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

次のコードで目標を達成しました。キーを押すとタイマーが停止します。更新 2pollInput別のIO唯一の機能に分離することができました。

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を押すと終了します。(ただし、何も出力されません):

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

これは私のオリジナルよりもはるかに優れていると思いますが、それが良い習慣であるかどうかはまだ完全には確信が持てません.

4

2 に答える 2

3

入力と出力をブロックしたくない場合は、入力と出力をブロックしないでください。ネットワイヤをイベントに接続する方法を示すために、ワイヤを実行するための小さなフレームワークを作成します。IOすべてを別々のスレッドで実行することにより、ワイヤのステッピングをブロックしないようにします。

netwireのドキュメント からEvent、フレームワークを開発している場合は sを分解することが許可されています。

Eventデフォルトでは、Netwire はそのタイプのコンストラクターをエクスポートしません。フレームワーク開発者は、モジュールをインポートしControl.Wire.Unsafe.Eventて独自のイベントを実装できます。

これによりEvent

data Event a = NoEvent | Event a

m入力用に 1 つのアクションを使用し、出力用に1 つのアクションを使用する非常に単純なフレームワークを作成します。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 は、入力スレッド、出力スレッド、ワイヤ スレッドの 3 つのスレッドを実行します。それらは、 をアトミックに変更することによって相互に通信します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
于 2015-06-23T04:47:14.387 に答える
0

まず、Netwire 5 の Kleisli Arrow を指しますか? . モナドとアローを理解しようと長い間試みた後、私はその答えを思いつきました。すぐに 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

終了の情報を実際に使用するにはrunWire、 type のワイヤを実行する特別な (しかし非常に単純な) 関数を作成する必要があり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)

もちろん、矢印構文を使用できます。

mainWire = proc _ -> do 
  c <- inputWire -< ()
  q <- quitWire -< c
  outputWire -< c
  returnA -< q

バージョンの方が速いかどうかはprocわかりませんが、この単純な例では、どちらもかなり読みやすいです。

から入力を取得し、それを と の両方にinputWireフィー​​ドして、タプルを取得します。次に、最初のものを最終出力として取得します。quitWireoutputWire(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
于 2015-09-29T05:09:25.880 に答える