Интерактивность консоли в 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 ответа:
Если вы не хотите блокировать вход и выход, не блокируйте вход и выход. Чтобы продемонстрировать, как подключить 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