当您只有 1 个Event或多个同时发生的事件或多个相同类型的事件时,很容易union
将它们组合成一个结果事件,然后传递给reactimate
并立即输出它。但是,如果您有 2 种不同类型的 2 种事件在不同时间发生怎么办?然后将它们组合成一个结果事件,您可以传递给它reactimate
成为不必要的复杂性。
我建议您实际尝试并使用只有事件而没有行为的反应香蕉来实现 FRP 解释中的合成器,您会很快看到行为简化了不必要的事件操作。
假设我们有 2 个事件,输出 Octave(类型为 Int 的同义词)和 Pitch(类型为 Char 的同义词)。用户按a至键g设置当前音高,或按+或-增加或减少当前八度。程序应输出当前音高和当前八度音程,如a0
、b2
或f7
。假设用户在不同时间以各种组合按下这些键,所以我们最终得到了 2 个事件流(Events),如下所示:
+ - + -- octave stream (time goes from left to right)
b c -- pitch stream
每次用户按下一个键,我们都会输出当前的八度和音高。但是结果事件应该是什么?假设默认音高为a
,默认八度为0
。我们最终应该得到一个如下所示的事件流:
a1 b1 b0 c0 c1 -- a1 corresponds to + event, b1 to b, b0 to -, etc
简单的字符输入/输出
让我们尝试从头开始实现合成器,看看我们是否可以在没有行为的情况下做到这一点。我们先写一个程序,放一个字符,按Enter,程序输出它,然后再次要求输入一个字符:
import System.IO
import Control.Monad (forever)
main :: IO ()
main = do
-- Terminal config to make output cleaner
hSetEcho stdin False
hSetBuffering stdin NoBuffering
-- Event loop
forever (getChar >>= putChar)
简单事件网络
让我们做上面的事情,但是用一个事件网络来说明它们。
import Control.Monad (forever)
import System.IO (BufferMode(..), hSetEcho, hSetBuffering, stdin)
import Control.Event.Handler (newAddHandler)
import Reactive.Banana
import Reactive.Banana.Frameworks
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription myAddHandler = do
event <- fromAddHandler myAddHandler
reactimate $ putChar <$> event
main :: IO ()
main = do
-- Terminal config to make output cleaner
hSetEcho stdin False
hSetBuffering stdin NoBuffering
-- Event loop
(myAddHandler, myHandler) <- newAddHandler
network <- compile (makeNetworkDescription myAddHandler)
actuate network
forever (getChar >>= myHandler)
网络是您的所有事件和行为发生并相互交互的地方。他们只能在Moment
单子上下文中做到这一点。在教程函数响应式编程启动指南中,事件网络的类比是人脑。人脑是所有事件流和行为相互交错的地方,但访问大脑的唯一方法是通过充当事件源(输入)的受体。
现在,在我们继续之前,请仔细检查上述代码片段中最重要的函数的类型:
type Handler a = a -> IO ()
newtype AddHandler a = AddHandler { register :: Handler a -> IO (IO ()) }
newAddHandler :: IO (AddHandler a, Handler a)
fromAddHandler :: Frameworks t => AddHandler a -> Moment t (Event t a)
reactimate :: Frameworks t => Event t (IO ()) -> Moment t ()
compile :: (forall t. Frameworks t => Moment t ()) -> IO EventNetwork
actuate :: EventNetwork -> IO ()
因为我们使用了最简单的 UI——字符输入/输出,所以我们将使用Control.Event.Handler
Reactive-banana 提供的模块。通常,GUI 库会为我们完成这项肮脏的工作。
type 的函数Handler
只是一个 IO 动作,类似于其他 IO 动作,例如getChar
or putStrLn
(例如后者有 type String -> IO ()
)。一个类型的函数Handler
接受一个值并用它执行一些 IO 计算。因此它只能在 IO 上下文中使用(例如 in main
)。
从类型很明显(如果您了解 monad 的基础知识),fromAddHandler
andreactimate
只能在Moment
上下文中使用(例如makeDescriptionNetwork
),而newAddHandler
, compile
andactuate
只能在IO
上下文中使用(例如main
)。
您创建一对类型值AddHandler
并Handler
使用newAddHandler
in main
,将此新AddHandler
函数传递给您的事件网络函数,您可以在其中使用fromAddHandler
. 您可以随心所欲地操作此事件流,然后将其事件包装在一个 IO 操作中,并将生成的事件流传递给reactimate
.
过滤事件
现在让我们只输出一些东西,如果用户按下+或-。让我们在用户按下时输出 1,当用户按下时输出+-1 -。(其余代码保持不变)。
action :: Char -> Int
action '+' = 1
action '-' = (-1)
action _ = 0
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription myAddHandler = do
event <- fromAddHandler myAddHandler
let event' = action <$> filterE (\e -> e=='+' || e=='-') event
reactimate $ putStrLn . show <$> event'
+如果用户按下or旁边的任何内容,我们不会输出-,因此更简洁的方法是:
action :: Char -> Maybe Int
action '+' = Just 1
action '-' = Just (-1)
action _ = Nothing
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription myAddHandler = do
event <- fromAddHandler myAddHandler
let event' = filterJust . fmap action $ event
reactimate $ putStrLn . show <$> event'
事件操作的重要功能(更多信息请参阅Reactive.Banana.Combinators
):
fmap :: Functor f => (a -> b) -> f a -> f b
union :: Event t a -> Event t a -> Event t a
filterE :: (a -> Bool) -> Event t a -> Event t a
accumE :: a -> Event t (a -> a) -> Event t a
filterJust :: Event t (Maybe a) -> Event t a
累积增量和减量
但是我们不想只输出 1 和 -1,我们想增加和减少值并在按键之间记住它!所以我们需要accumE
。accumE
接受一个值和一个类型的函数流(a -> a)
。每次从该流中出现一个新函数时,都会将其应用于该值,并记住结果。下次出现新函数时,将其应用于新值,依此类推。这使我们能够记住,我们当前必须减少或增加哪个数字。
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription myAddHandler = do
event <- fromAddHandler myAddHandler
let event' = filterJust . fmap action $ event
functionStream = (+) <$> event' -- is of type Event t (Int -> Int)
reactimate $ putStrLn . show <$> accumE 0 functionStream
functionStream
基本上是一个函数流(+1)
, (-1)
, (+1)
, 取决于用户按下的键。
联合两个事件流
现在我们准备好实现原始文章中的八度音阶和音高。
type Octave = Int
type Pitch = Char
actionChangeOctave :: Char -> Maybe Int
actionChangeOctave '+' = Just 1
actionChangeOctave '-' = Just (-1)
actionChangeOctave _ = Nothing
actionPitch :: Char -> Maybe Char
actionPitch c
| c >= 'a' && c <= 'g' = Just c
| otherwise = Nothing
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription addKeyEvent = do
event <- fromAddHandler addKeyEvent
let eChangeOctave = filterJust . fmap actionChangeOctave $ event
eOctave = accumE 0 ((+) <$> eChangeOctave)
ePitch = filterJust . fmap actionPitch $ event
eResult = (show <$> ePitch) `union` (show <$> eOctave)
reactimate $ putStrLn <$> eResult
我们的程序将输出当前音高或当前八度音程,具体取决于用户按下的内容。它还将保留当前八度的值。可是等等!这不是我们想要的!如果我们想输出当前音高和当前八度音阶,每次用户按下一个字母+或时-怎么办?
在这里它变得超级难。我们不能合并 2 个不同类型的事件流,所以我们可以将它们都转换为Event t (Pitch, Octave)
. 但是如果一个音高事件和一个八度事件发生在不同的时间(即它们不是同时发生的,这在我们的例子中实际上是确定的),那么我们的临时事件流宁愿有 type Event t (Maybe Pitch, Maybe Octave)
,而Nothing
在任何地方你都没有相应的事件。因此,如果用户按顺序按下+ b - c +,并且我们假设默认八度为 0,默认音高为 0 a
,那么我们最终会得到一系列对[(Nothing, Just 1), (Just 'b', Nothing), (Nothing, Just 0), (Just 'c', Nothing), (Nothing, Just 1)]
,包裹在Event
.
然后我们必须弄清楚如何Nothing
用当前音高或八度音进行替换,因此结果序列应该类似于[('a', 1), ('b', 1), ('b', 0), ('c', 0), ('c', 1)]
.
这太低级了,当有高级抽象可用时,真正的程序员不应该担心对齐这样的事件。
行为简化了事件操作
一些简单的修改,我们得到了同样的结果。
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription addKeyEvent = do
event <- fromAddHandler addKeyEvent
let eChangeOctave = filterJust . fmap actionChangeOctave $ event
bOctave = accumB 0 ((+) <$> eChangeOctave)
ePitch = filterJust . fmap actionPitch $ event
bPitch = stepper 'a' ePitch
bResult = (++) <$> (show <$> bPitch) <*> (show <$> bOctave)
eResult <- changes bResult
reactimate' $ (fmap putStrLn) <$> eResult
将音高事件转换为行为stepper
并替换accumE
为accumB
以获得八度音程行为而不是八度音程事件。要获得结果行为,请使用applicative style。
然后,要获取必须传递给 的事件reactimate
,请将生成的 Behavior 传递给changes
。但是,changes
返回一个复杂的一元值Moment t (Event t (Future a))
,因此您应该使用reactimate'
而不是reactimate
. 这也是为什么你必须putStrLn
在上面的例子中提升两次 into的eResult
原因,因为你将它提升到Future
functor 内的Event
functor。
查看我们在此处使用的函数的类型,以了解其中的内容:
stepper :: a -> Event t a -> Behavior t a
accumB :: a -> Event t (a -> a) -> Behavior t a
changes :: Frameworks t => Behavior t a -> Moment t (Event t (Future a))
reactimate' :: Frameworks t => Event t (Future (IO ())) -> Moment t ()