更简单的 Reactive 替代库?(哈斯克尔) [英] Simpler alternative libs to Reactive? (Haskell)

查看:18
本文介绍了更简单的 Reactive 替代库?(哈斯克尔)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在学习 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

状态存储在IORefs中,这使得它看起来就像命令式语言.

The states are stored in IORefs, 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 Events 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 aBehavior 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.

EventBehavior 的大部分接口都隐藏在它们的实例中.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 Behaviors 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 提供的其他功能 提供无法用基本类型类表示的功能.这些引入了状态性,将 Events 组合在一起,并在 Events 和 Behaviors 之间转换.

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 Events together, and convert between Events and Behaviors.

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

EventBehavior 的实例以及 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 Events 和Behaviors.该示例使用了四个 GLUT 事件 - reshapeCallbackkeyboardMouseCallbackidleCallbackdisplayCallback.我们将这些映射到 Events 和 Behaviors.

The framework interface

Our framework will map the IO events from GLUT to reactive-banana Events and Behaviors. There are four GLUT events that the example uses - reshapeCallback, keyboardMouseCallback, idleCallback, and displayCallback. We will map these to Events and Behaviors.

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.我们将在接下来的两节中定义 programreactiveGLUT.

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 IORefs 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 将被 positionChangeangleSpeedChange 完全取代.这些将 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 到当前的 timeangle,并在每次出现 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 Events from IO and run IO actions in response to Events. We can make Behaviors from Events and poll Behaviors when Events 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)>Inputs 和一个 Event 用于轮询输出 display.我们执行少量处理以简化时钟.我们将program 应用到我们准备获得程序的Outputsinputs.使用 <@,我们在显示事件发生时轮询 display.最后,reactimate 告诉reactive-banana 在相应的Event 发生时运行setDisplayaddWhenIdle.一旦我们描述了网络,我们就编译启动它.

Our whole network consists of the following parts. First we create Events (using fromAddHandler) or Behaviors (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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆