Интерактивность консоли в Netwire?


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

Обновление 1/2

Я добился своей цели, работая с помощью следующий код. Таймер останавливается при любом нажатии клавиши. Update 2 мне удалось выделить pollInput в другую 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
Я думаю, что это намного лучше, чем мой оригинал, но я все еще не вполне уверен, является ли это хорошей практикой или нет.
2 4

2 ответа:

Если вы не хотите блокировать вход и выход, не блокируйте вход и выход. Чтобы продемонстрировать, как подключить netwire к событиям, мы сделаем небольшую структуру для запуска проводов. Мы избежим блокировки шага проволоки, выполнив все IO в отдельных потоках.

Из документации netwire, нам разрешено деконструировать Event s, Если мы разрабатываем фреймворк.

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

Основной поток устанавливает очереди, порождает потоки ввода-вывода, запускает провод и сигнализирует потокам ввода-вывода, когда программа имеет остановился.

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

Во-первых, я бы указал на стрелку Клейсли в Netwire 5?. Я пришел к этому ответу после долгих попыток понять монады и стрелы. я скоро приведу минимальный пример, используя провод Клейсли.

Эта программа просто повторяет то, что пользователь вводит, и завершает работу, когда он попадает в 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

Это конструктор проводов Клейсли, написанный в ответе в посте упоминаемый. Таким образом, эта функция поднимает любую функцию Клейсли a -> m b в Wire s e m a b. Это ядро любого ввода-вывода, который мы делаем в этой программе.

Поскольку мы повторяем как типы пользователей, 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, которая запускает провод типа 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, передаем их как 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