I've been wanting to give FRP a shot for a while now, and yesterday I finally bit the bullet and had a go, using Netwire 5 to begin with (a fairly arbitrary choice in itself, but I have to start somewhere!). I've managed to get to the point of "code which works" but I've noticed a couple of patterns which I'm not sure are part of how the library is expected to be used or whether they are a symptom that I'm doing something wrong somewhere.
I started with this tutorial, which was enough to get me up and running pretty easily -- I now have a spinning cube controlled by a simple "incrementing number" wire:
spin :: (HasTime t s, Monad m) => Wire s e m a GL.GLfloat
spin = integral 0 . 5
and the application will quit when "Esc" is pressed, making use of the wires supplied in netwire-input-glfw:
shouldQuit :: (Monoid e, Functor m, Monad m) => Wire s e (GLFWInputT m) a a
shouldQuit = keyPressed GLFW.Key'Escape
An important distinction between these is that spin
never inhibits -- it should always return some value -- while shouldQuit
inhibits all the time; until the key is actually pressed in which case I quit the application.
The thing that makes me uneasy is the way I end up having to use these wires. Right now, it looks something like this:
(wt', spinWire') <- stepWire spinWire st $ Right undefined
((qt', quitWire'), inp'') <- runStateT (stepWire quitWire st $ Right undefined) inp'
case (qt', wt') of
(Right _, _) -> return ()
(_, Left _) -> return () -- ???
(_, Right x) -> --do things, render, recurse into next frame
There are two things about this pattern that make me feel uncomfortable. First, the fact that I pass Right undefined
to both calls to stepWire
. I think (if my understanding is correct) that this parameter is for sending events to a wire, and that since my wires don't make any use of events this is "safe", but it feels bad (EDIT maybe "events" is the wrong word here -- the tutorial describes it as for "blocking values", but the point still stands -- I never intend to block and don't make use of the e
parameter anywhere in my wire). I looked to see if there was a version of stepWire
for the situation where you know you never have an event and you wouldn't respond to it even if you did have one, but couldn't see one. I tried making the wires e
parameter ()
and then passing Right ()
everywhere, which feels marginally less dirty than undefined
, but still doesn't quite seem to represent my intent.
Similarly, the return value is also an Either
. That's perfect for the shouldQuit
wire, but notice I have to pattern match on wt'
, the output of the spin
wire. I really don't know what it would mean for that to inhibit, so I just return ()
, but I can imagine this getting unwieldy as the number of wires increases, and again, it just doesn't seem that representative of my intent -- to have a wire which never inhibits and which I can rely upon always to hold the next value.
So although I have code which works, I'm left with the uneasy feeling that I'm "doing it wrong" somehow, and since Netwire 5 is fairly new it's difficult to find examples of "idiomatic" code that I can check against and see if I'm near the mark. Is this how the library is intended to be used or am I missing something?
EDIT: I've managed to resolve the second problem I mention (pattern matching on the Either
result of spin
) by combining spin
and shouldQuit
into a single Wire
:
shouldContinuePlaying :: (Monoid e, Functor m, Monad m) => Wire s e (GLFWInputT m) a a
shouldContinuePlaying = keyNotPressed GLFW.Key'Escape
game :: (HasTime t s, Monoid e, Functor m, Monad m) => Wire s e (GLFWInputT m) a GL.GLfloat
game = spin . shouldContinuePlaying
Stepping this wire gives me a much more sensible return value -- if it's Left
I can quit, otherwise I have the piece of data to work with. It also hints at a greater degree of composability than my original method.
I still have to pass Right undefined
as the input to this new wire though. Admittedly, there's only one of them now, but I'm still not sure if this is the right approach.
At the very top level of your program, you're going to have some wire that has (abbreviated) type Wire a b
. This is going to need to be passed something of type a
and it will return something of type b
every time you take a step. For example, both a
and b
can be some WorldState
for a game or maybe [RigidBody]
for a physics simulator. In my opinion, it's OK to pass Right undefined
at the top level.
That being said, you're ignoring the important Alternative
instance of Wire a b
for the input wires. It provides an operator <|>
that works in a very nice way:
Suppose we have two wires:
w1 :: Wire a b
w2 :: Wire a b
If w1 inhibits, then
w1 <|> w2 == w2
If w1 doesn't inhibit, then
w1 <|> w2 == w1
This means that w1 <|> w2
will only inhibit if both w1
and w2
inhibit. This is great, this means that we can do things like:
spin :: (HasTime t s, Monad m) => Wire s e m a GL.GLfloat
spin = integral 0 . (10 . keyPressed GLFW.Key'F <|> 5)
When you press F
, you will spin twice as fast!
If you want to change the semantics of a wire after pressing a button, you have to be a bit more creative, but not much. If your wire behaves differently, it means you're doing some sort of switch. The documentation for switches mostly requires you to follow the types.
Here's a wire that will act like the identity wire until you press the given key, and will then inhibit forever:
trigger :: GLFW.Key -> GameWire a a
trigger key =
rSwitch mkId . (mkId &&& ((now . pure mkEmpty . keyPressed key) <|> never))
With this you can do cool things like:
spin :: (HasTime t s, Monad m) => Wire s e m a GL.GLfloat
spin = integral 0 . spinSpeed
where
spinSpeed = 5 . trigger GLFW.Key'F -->
-5 . trigger GLFW.Key'F -->
spinSpeed
This will toggle the spinner between going forwards and backwards whenever you hit F
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With