更简单的 Reactive 替代库?(哈斯克尔) [英] Simpler alternative libs to Reactive? (Haskell)
问题描述
我正在学习 Haskell,并尝试编写一些事件驱动的程序.
I'm learning Haskell, and trying to write some event-driven programs.
以下代码来自教程:http://www.haskell.org/haskellwiki/OpenGLTutorial2
main = do
(progname,_) <- getArgsAndInitialize
initialDisplayMode $= [DoubleBuffered]
createWindow "Hello World"
reshapeCallback $= Just reshape
angle <- newIORef (0.0::GLfloat) -- 1
delta <- newIORef (0.1::GLfloat) -- 2
position <- newIORef (0.0::GLfloat, 0.0) -- 3
keyboardMouseCallback $= Just (keyboardMouse delta position)
idleCallback $= Just (idle angle delta)
displayCallback $= (display angle position)
mainLoop
状态存储在IORef
s中,这使得它看起来就像命令式语言.
The states are stored in IORef
s, which makes it looks just like imperative language.
我听说除了这个 Graphics.UI.GLUT
之外还有其他 API(例如 Reactive
),但它看起来很复杂.
I'v heard that there are APIs other than this Graphics.UI.GLUT
, (e.g. Reactive
), but it looks very complicated.
我的方法是lib提供一个函数runEventHandler
,用户编写一个handler
,它接受Event
的列表并转换它们到IO()
.
My approach is that the lib provide a function runEventHandler
, and the user writes a handler
that accepts list of Event
s and convert them to IO ()
.
handler :: [Event] -> IO ()
runEventHandler :: ( [Event] -> IO () ) -> IO ()
main
函数应该是这样的:
main = runEventHandler handler
有这样的库吗?
我目前正在使用多线程实现一个,但我担心它的性能可能很差......
I am currently implementing one using multi-threading, but I'm worrying that it might be poor in performance ...
推荐答案
reactive-banana 是一个成熟的库非常类似于 reactive.我们不会尝试重新发明 frp 库;相反,我们将探索如何将反应式香蕉集成到我们自己的项目中.
reactive-banana is a mature library very similar to reactive. We won't try to reinvent an frp library; instead we'll explore how to integrate reactive-banana into a project for ourselves.
为了在OpenGL中使用reactive-banana这样的函数式反应式编程库,我们将把工作分成4部分,其中2部分已经存在.我们将使用现有的 GLUT 库与 OpenGL 进行交互,并使用现有的反应式香蕉库来实现函数式反应式编程.我们将提供我们自己的 2 个部分.我们将提供的第一部分是将 GLUT 连接到反应式香蕉的框架.我们将提供的第二部分是将根据 frp 实现(reactive-banana)和框架以及 GLUT 类型编写的程序.
To use a functional reactive programming library like reactive-banana with OpenGL we will divide the work into 4 parts, 2 of which already exist. We will use the existing GLUT library to interact with OpenGL, and the existing reactive-banana library for an implementation of functional reactive programming. We will provide 2 parts of our own. The first part we will provide is a framework that will connect GLUT to reactive-banana. The second part we will provide is the program that will be written in terms of the frp implementation (reactive-banana) and framework and GLUT types.
我们提供的两个部分都将根据reactive-banana frp 库编写.该库有两大思想,Event t a
和 Behavior t a
.Event t a
表示在不同时间点发生的带有 a
类型数据的事件.Behavior t a
表示在所有时间点定义的 a
类型的时变值.类型系统要求我们保留 t
类型参数,否则忽略.
Both of the parts that we provide will be written in terms of the reactive-banana frp library. The library has two big ideas, Event t a
and Behavior t a
. Event t a
represents events carrying data of type a
that occur at different points in time. Behavior t a
represents a time varying value of type a
that is defined at all points in time. The t
type argument we are required by the type system to preserve but otherwise ignore.
Event
和 Behavior
的大部分接口都隐藏在它们的实例中.Event
是一个 Functor
- 我们可以 fmap
或 <$>
对任何 <代码>事件代码>.
Most of the interface to Event
and Behavior
are hidden in their instances. Event
is a Functor
- we can fmap
or <$>
a function over the values of any Event
.
fmap :: (a -> b) -> Event t a -> Event t b
Behavior
既是 Applicative
又是 Functor
.我们可以 fmap
或 <$>
一个函数覆盖 Behavior
所采用的所有值,可以用 提供新的不变的值>pure
,并用 <*>
计算新的 Behavior
.
Behavior
is both Applicative
and a Functor
. We can fmap
or <$>
a function over all the values a Behavior
takes on, can provide new constant unchanging values with pure
, and calculate new Behavior
s with <*>
.
fmap :: (a -> b) -> Behavior t a -> Behavior t b
pure :: a -> Behavior t a
<*> :: Behavior t (a -> b) -> Behavior t a -> Behavior t b
还有一些 reactive-banana 提供的其他功能 提供无法用基本类型类表示的功能.这些引入了状态性,将 Event
s 组合在一起,并在 Event
s 和 Behavior
s 之间转换.
There are a few other functions provided by reactive-banana that provide functionality that can't be represented in terms of base typeclasses. These introduce statefulness, combine Event
s together, and convert between Event
s and Behavior
s.
State 由 accumE
引入,它采用初始值和从前一个值到新值的变化的 Event
并产生一个 Event
> 的新值.accumB
产生一个 Behavior
代替
State is introduced by accumE
which takes an initial value and an Event
of changes from the previous value to a new value and produces an Event
of the new values. accumB
produces a Behavior
instead
accumE :: a -> Event t (a -> a) -> Event t a
accumB :: a -> Event t (a -> a) -> Behavior t a
union
将两个事件流组合在一起
union
combines two event streams together
union :: Event t a -> Event t a -> Event t a
stepper
可以将 Event
转换为保存最新值的 Behavior
如果我们提供一个初始值以便完全定义它时间点.apply
或 <@>
可以将 Behavior
转换为 Event
如果我们提供一系列 Events
轮询 Behavior
的当前值.
stepper
can convert an Event
to a Behavior
holding the most recent value if we provide an initial value so that it is defined at all points in time. apply
or <@>
can convert a Behavior
into an Event
if we provide a series of Events
at which to poll the current value of the Behavior
.
stepper :: a -> Event t a -> Behavior t a
<@> :: Behavior t (a -> b) -> Event t a -> Event t b
Event
和 Behavior
的实例以及 Reactive.Banana.Combinators 构成了函数式反应式编程的整个界面.
The instances for Event
and Behavior
and the 19 functions in Reactive.Banana.Combinators make up the entire interface for functional reactive programming.
总的来说,我们将需要 GLUT 库和我们正在实现的 OpenGL 示例使用的库、reactive-banana 库、用于制作框架的reactive-banana 导出和 RankNTypes 扩展、一些线程间通信机制,以及能够读取系统时钟.
Overall, we will need the GLUT library and libraries used by the OpenGL example we are implementing, the reactive-banana library, the reactive-banana exports for making frameworks and the RankNTypes extension, a couple mechanisms for interthread communication, and the ability to read the system clock.
{-# LANGUAGE RankNTypes #-}
import Graphics.UI.GLUT
import Control.Monad
import Reactive.Banana
import Reactive.Banana.Frameworks
import Data.IORef
import Control.Concurrent.MVar
import Data.Time
框架接口
我们的框架会将IO
事件从GLUT 映射到reactive-banana Event
s 和Behavior
s.该示例使用了四个 GLUT 事件 - reshapeCallback
、keyboardMouseCallback
、idleCallback
和 displayCallback
.我们将这些映射到 Event
s 和 Behavior
s.
The framework interface
Our framework will map the IO
events from GLUT to reactive-banana Event
s and Behavior
s. There are four GLUT events that the example uses - reshapeCallback
, keyboardMouseCallback
, idleCallback
, and displayCallback
. We will map these to Event
s and Behavior
s.
reshapeCallback
在用户调整窗口大小时运行.作为回调,它需要类型 type ReshapeCallback = Size ->IO()
.我们将其表示为 Event t Size
.
reshapeCallback
is run when the user resizes the window. As a callback, it required something of the type type ReshapeCallback = Size -> IO ()
. We will represent this as an Event t Size
.
keyboardMouseCallback
在用户提供键盘输入、移动鼠标或单击鼠标按钮时运行.作为回调,它需要类型为 type KeyboardMouseCallback = Key ->密钥状态 ->修饰符 ->位置 ->IO()
.我们将其表示为类型为 Event t KeyboardMouse
的输入,其中 KeyboardMouse
将传递给回调的所有参数捆绑在一起.
keyboardMouseCallback
is run when the user provides keyboard input, moves the mouse, or clicks a mouse button. As a callback, it required something of the type type KeyboardMouseCallback = Key -> KeyState -> Modifiers -> Position -> IO ()
. We will represent this as an input with type Event t KeyboardMouse
, where KeyboardMouse
bundles together all of the arguments passed to the callback.
data KeyboardMouse = KeyboardMouse {
key :: Key,
keyState :: KeyState,
modifiers :: Modifiers,
pos :: Position
}
idleCallback
在时间过去时运行.我们将其表示为跟踪已过去时间量的行为,Behavior t DiffTime
.因为它是一个Behavior
而不是一个Event
,我们的程序将无法直接观察时间的流逝.如果不需要,我们可以使用 Event
代替.
idleCallback
is run when time passes. We will represent this as a behavior that tracks the amount of time that has passed, Behavior t DiffTime
. Because it is a Behavior
instead of an Event
, our program won't be able to directly observe time passing. If this isn't desired, we could use an Event
instead.
将我们得到的所有输入捆绑在一起
Bundling all of the inputs together we get
data Inputs t = Inputs {
keyboardMouse :: Event t KeyboardMouse,
time :: Behavior t DiffTime,
reshape :: Event t Size
}
displayCallback
与其他回调不同;它不是用于程序的输入,而是用于输出需要显示的内容.由于 GLUT 可以随时运行它以尝试在屏幕上显示某些内容,因此在所有时间点都对其进行定义是有意义的.我们将用 Behavior t DisplayCallback
表示这个输出.
displayCallback
is different from the other callbacks; it isn't for the input to the program, but instead is for outputting what needs to be displayed. Since GLUT could run this at any time to try to display something on the screen, it makes sense for it to be defined at all points in time. We will represent this output with a Behavior t DisplayCallback
.
我们还需要一个输出——为了响应事件,示例程序偶尔会产生其他 IO 操作.我们将允许程序通过 Event t (IO ())
引发事件以执行任意 IO.
There is one more output we will need - in response to events the example program occasionally produces other IO actions. We will allow the program to raise events to execute arbitrary IO with an Event t (IO ())
.
将两个输出捆绑在一起我们得到
Bundling both outputs together we get
data Outputs t = Outputs {
display :: Behavior t DisplayCallback,
whenIdle :: Event t (IO ())
}
我们的框架将通过传递一个类型为 forall t 的程序来调用.输入 t ->输出 t
.我们将在接下来的两节中定义 program
和 reactiveGLUT
.
Our framework will be invoked by passing it a program with the type forall t. Inputs t -> Outputs t
. We will define program
and reactiveGLUT
in the next two sections.
main :: IO ()
main = do
(progname,_) <- getArgsAndInitialize
initialDisplayMode $= [DoubleBuffered]
createWindow "Hello World"
reactiveGLUT program
程序
程序将使用reactive-banana 将Inputs
映射到Outputs
.要开始移植教程代码,我们将从 cubes
中删除 IORef
并将 reshape
重命名为 onReshape
因为它与我们框架接口中的名称冲突.
The program
The program will use reactive-banana to map the Inputs
to the Outputs
. To get started porting the tutorial code, we'll remove the IORef
s from cubes
and rename reshape
to onReshape
since it conflicts with a name from our framework interface.
cubes :: GLfloat -> (GLfloat, GLfloat) -> DisplayCallback
cubes a (x',y') = do
clear [ColorBuffer]
loadIdentity
translate $ Vector3 x' y' 0
preservingMatrix $ do
rotate a $ Vector3 0 0 1
scale 0.7 0.7 (0.7::GLfloat)
forM_ (points 7) $ \(x,y,z) -> preservingMatrix $ do
color $ Color3 ((x+1)/2) ((y+1)/2) ((z+1)/2)
translate $ Vector3 x y z
cube 0.1
swapBuffers
onReshape :: ReshapeCallback
onReshape size = do
viewport $= (Position 0 0, size)
keyboardMouse
将被 positionChange
和 angleSpeedChange
完全取代.这些将 KeyboardMouse
事件转换为改变立方体旋转的位置或速度.当事件不需要更改时,它们返回 Nothing
.
keyboardMouse
will be completely replaced by positionChange
and angleSpeedChange
. These convert a KeyboardMouse
event into a change to make to either the position or the speed the cubes are rotating. When no change is needed for an event, they return Nothing
.
positionChange :: Fractional a => KeyboardMouse -> Maybe ((a, a) -> (a, a))
positionChange (KeyboardMouse (SpecialKey k) Down _ _) = case k of
KeyLeft -> Just $ \(x,y) -> (x-0.1,y)
KeyRight -> Just $ \(x,y) -> (x+0.1,y)
KeyUp -> Just $ \(x,y) -> (x,y+0.1)
KeyDown -> Just $ \(x,y) -> (x,y-0.1)
_ -> Nothing
positionChange _ = Nothing
angleSpeedChange :: Num a => KeyboardMouse -> Maybe (a -> a)
angleSpeedChange (KeyboardMouse (Char c) Down _ _) = case c of
' ' -> Just negate
'+' -> Just (+1)
'-' -> Just (subtract 1)
_ -> Nothing
angleSpeedChange _ = Nothing
计算位置相当容易,我们累积键盘输入的变化.filterJust :: Event t (Maybe a) ->事件 t a
抛出我们不感兴趣的事件.
Calculating the position is fairly easy, we accumulate the changes from the keyboard inputs. filterJust :: Event t (Maybe a) -> Event t a
throws out the events that we weren't interested in.
positionB :: Fractional a => Inputs t -> Behavior t (a, a)
positionB = accumB (0.0, 0.0) . filterJust . fmap positionChange . keyboardMouse
我们将以稍微不同的方式计算旋转立方体的角度.我们会记住速度变化时的时间和角度,将计算角度差的函数应用于时间差,并将其添加到初始角度.
We'll calculate the angle of the rotating cubes a bit differently. We'll remember the time and angle when the speed changes, apply a function that calculates the difference in angle to the difference in times, and add that to the initial angle.
angleCalculation :: (Num a, Num b) => a -> b -> (a -> b) -> a -> b
angleCalculation a0 b0 f a1 = f (a1 - a0) + b0
计算angle
有点困难.首先我们计算一个事件,angleF :: Event t (DiffTime -> GLfloat)
,保存一个从时间差到角度差的函数.我们提升并应用我们的 angleCalculation
到当前的 time
和 angle
,并在每次出现 angleF
时轮询它事件.我们用 stepper
将轮询函数转换为 Behavior
并将其应用到当前 time
.
Calculating the angle
is a bit more difficult. First we compute an event, angleF :: Event t (DiffTime -> GLfloat)
, holding a function from a difference between times to a difference between angles. We lift and apply our angleCalculation
to the current time
and angle
, and poll that at each occurrence of the angleF
event. We convert the polled function into a Behavior
with stepper
and apply it to the current time
.
angleB :: Fractional a => Inputs t -> Behavior t a
angleB inputs = angle
where
initialSpeed = 2
angleSpeed = accumE initialSpeed . filterJust . fmap angleSpeedChange . keyboardMouse $ inputs
scaleSpeed x y = 10 * x * realToFrac y
angleF = scaleSpeed <$> angleSpeed
angleSteps = (angleCalculation <$> time inputs <*> angle) <@> angleF
angle = stepper (scaleSpeed initialSpeed) angleSteps <*> time inputs
整个程序
将Inputs
映射到Outputs
.它表示 display
的行为是 cubes
提升并应用于角度和位置.其他IO
副作用的Event
是每次reshape
事件发生时onReshape
.
The whole program
maps Inputs
to Outputs
. It says that the behavior for what to display
is cubes
lifted and applied to the angle and position. The Event
for other IO
side effects is onReshape
every time the reshape
event happens.
program :: Inputs t -> Outputs t
program inputs = outputs
where
outputs = Outputs {
display = cubes <$> angleB inputs <*> positionB inputs,
whenIdle = onReshape <$> reshape inputs
}
框架
我们的框架接受类型为 forall t 的程序.输入 t ->输出 t
并运行它.为了实现这个框架,我们使用了 Reactive.Banana.Frameworks
中的函数.这些函数允许我们从 IO
引发 Event
并运行 IO
动作以响应 Event
.我们可以从 Event
生成 Behavior
并使用 Event
中的函数在 Event
发生时轮询 Behavior
>Reactive.Banana.Combinators.
The framework
Our framework accepts a program with the type forall t. Inputs t -> Outputs t
and runs it. To implement the framework, we use the functions in Reactive.Banana.Frameworks
. These functions allow us to raise Event
s from IO
and run IO
actions in response to Event
s. We can make Behavior
s from Event
s and poll Behavior
s when Event
s occur using the functions from Reactive.Banana.Combinators
.
reactiveGLUT :: (forall t. Inputs t -> Outputs t) -> IO ()
reactiveGLUT program = do
-- Initial values
initialTime <- getCurrentTime
-- Events
(addKeyboardMouse, raiseKeyboardMouse) <- newAddHandler
(addTime, raiseTime) <- newAddHandler
(addReshape, raiseReshape) <- newAddHandler
(addDisplay, raiseDisplay) <- newAddHandler
newAddHandler
创建一个处理 Event t a
的句柄,以及一个引发 a -> 类型事件的函数.IO()
.我们为键盘和鼠标输入、空闲时间传递和窗口形状变化制作了明显的事件.我们还创建了一个事件,当我们需要在 displayCallback
中运行它时,我们将使用它来轮询 display
Behavior
.
newAddHandler
creates a handle with which to talk about an Event t a
, and a function to raise the event of type a -> IO ()
. We make the obvious events for keyboard and mouse input, idle time passing, and the window shape changing. We also make an event that we will use to poll the display
Behavior
when we need to run it in the displayCallback
.
我们有一个棘手的问题需要克服 - OpenGL 要求所有 UI 交互都发生在特定线程中,但我们不确定绑定到响应式香蕉事件的操作将发生在哪个线程中.我们将使用跨线程共享的几个变量,以确保 Output
IO
在 OpenGL 线程中运行.对于 display
输出,我们将使用 MVar
来存储轮询的 display
操作.对于在 whenIdle
中排队的 IO
操作,我们将在 IORef
中累积它们,
We have one tricky problem to overcome - OpenGL requires all the UI interaction to happen in a specific thread, but we aren't sure what thread the actions we bind to reactive-banana events will happen in. We'll use a couple of variables shared across threads to make sure the Output
IO
is run in the OpenGL thread. For display
output, we'll use an MVar
to store the polled display
action. For IO
actions that are queued in whenIdle
we'll accumulate them in an IORef
,
-- output variables and how to write to them
displayVar <- newEmptyMVar
whenIdleRef <- newIORef (return ())
let
setDisplay = putMVar displayVar
runDisplay = takeMVar displayVar >>= id
addWhenIdle y = atomicModifyIORef' whenIdleRef (\x -> (x >> y, ()))
runWhenIdle = atomicModifyIORef' whenIdleRef (\x -> (return (), x)) >>= id
我们的整个网络由以下部分组成.首先,我们为每个创建
和一个 Event
(使用fromAddHandler
)或Behavior
(使用fromChanges
)>InputsEvent
用于轮询输出 display
.我们执行少量处理以简化时钟.我们将program
应用到我们准备获得程序的Outputs
的inputs
.使用 <@
,我们在显示事件发生时轮询 display
.最后,reactimate
告诉reactive-banana 在相应的Event
发生时运行setDisplay
或addWhenIdle
.一旦我们描述了网络,我们就编译
并启动
它.
Our whole network consists of the following parts. First we create Event
s (using fromAddHandler
) or Behavior
s (using fromChanges
) for each of the Inputs
and an Event
for polling the output display
. We perform a small amount of processing to simplify the clock. We apply the program
to the inputs
we prepared to get the program's Outputs
. Using <@
, we poll the display
whenever our display event happens. Finally, reactimate
tells reactive-banana to run setDisplay
or addWhenIdle
whenever the corresponsonding Event
occurs. Once we have described the network we compile
and actuate
it.
-- Reactive network for GLUT programs
let networkDescription :: forall t. Frameworks t => Moment t ()
networkDescription = do
keyboardMouseEvent <- fromAddHandler addKeyboardMouse
clock <- fromChanges initialTime addTime
reshapeEvent <- fromAddHandler addReshape
displayEvent <- fromAddHandler addDisplay
let
diffTime = realToFrac . (flip diffUTCTime) initialTime <$> clock
inputs = Inputs keyboardMouseEvent diffTime reshapeEvent
outputs = program inputs
displayPoll = display outputs <@ displayEvent
reactimate $ fmap setDisplay displayPoll
reactimate $ fmap addWhenIdle (whenIdle outputs)
network <- compile networkDescription
actuate network
对于我们感兴趣的每个 GLUT 回调,我们会引发相应的 react-banana Event
.对于空闲回调,我们还运行任何排队的事件.对于显示回调,我们运行轮询的 DisplayCallback
.
For each of the GLUT callbacks we are interested in we raise the corresponding reactive-banana Event
. For the idle callback we also run any queued events. For the display callback, we run the polled DisplayCallback
.
-- Handle GLUT events
keyboardMouseCallback $= Just (\k ks m p -> raiseKeyboardMouse (KeyboardMouse k ks m p))
idleCallback $= Just (do
getCurrentTime >>= raiseTime
runWhenIdle
postRedisplay Nothing)
reshapeCallback $= Just raiseReshape
displayCallback $= do
raiseDisplay ()
runDisplay
mainLoop
示例的其余部分
其余的教程代码可以逐字重复
The rest of the example
The rest of the tutorial code can be repeated verbatim
vertex3f :: (GLfloat, GLfloat, GLfloat) -> IO ()
vertex3f (x, y, z) = vertex $ Vertex3 x y z
points :: Int -> [(GLfloat,GLfloat,GLfloat)]
points n = [ (sin (2*pi*k/n'), cos (2*pi*k/n'), 0) | k <- [1..n'] ]
where n' = fromIntegral n
cube :: GLfloat -> IO ()
cube w = renderPrimitive Quads $ mapM_ vertex3f
[ ( w, w, w), ( w, w,-w), ( w,-w,-w), ( w,-w, w),
( w, w, w), ( w, w,-w), (-w, w,-w), (-w, w, w),
( w, w, w), ( w,-w, w), (-w,-w, w), (-w, w, w),
(-w, w, w), (-w, w,-w), (-w,-w,-w), (-w,-w, w),
( w,-w, w), ( w,-w,-w), (-w,-w,-w), (-w,-w, w),
( w, w,-w), ( w,-w,-w), (-w,-w,-w), (-w, w,-w) ]
这篇关于更简单的 Reactive 替代库?(哈斯克尔)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!