Almost a Netwire 5 Tutorial

Notes

I’m writing this using Netwire 5.0.0. This tutorial may be out of date in the future. I am also by no means an expert in using Netwire, I’ve just recently learned it and wanted to practice my knowledge by writing something about it.

The source for this tutorial can be found on GitHub. I suggest looking around there after reading this tutorial.

This tutorial is going to be far more about the Netwire side of things than the OpenGL / GLFW side of things. The OpenGL will (probably) be out of date, so don’t use it as a reference for good and modern OpenGL practices. This is how I personally tend to set up an OpenGL context within a window (created by GLFW):

import Graphics.Rendering.OpenGL import Graphics.UI.GLFW import Data.IORef main :: IO () main = do initialize openWindow (Size 640 480) [DisplayRGBBits 8 8 8, DisplayAlphaBits 8, DisplayDepthBits 24] Window closedRef <- newIORef False windowCloseCallback $= do writeIORef closedRef True return True -- Hook up to the Netwire network closeWindow

I’m not really going to go in-depth about what each of all of these things do. There are tons and tons of blog posts and tutorials out there explaining GLFW.

I’m not really going to go in-depth about how to make the window, seeing as there are tons and tons of blog posts and tutorials out there explaining that. Instead, with your window created (that should flash open and then close), we’re ready to get into the interesting stuff.

The Theory

What is a wire? How does it help me? What the hell does it do for the sake of Functional Reactive Programming (FRP) networks, let alone graphics? It’s honestly nothing magical – nothing special even. It’s just a certain type of data structure used in a certain way. So let’s get started; a wire is a something:

data Wire s e m a b = ...

What something? Well… it doesn’t really matter. It’s a something that has a bunch of type parameters, we’ll refer to that for now. The thing I found most helpful when first learning is understanding what all of those different letters mean.

s = The step value. It usually references the time step (otherwise known as delta time) between calls to the wire.

= The step value. It usually references the time step (otherwise known as delta time) between calls to the wire. e = The blocking value. Wires can either produce a value to show that it’s functioning (the b type, listed below), or a value to show that it’s blocking. The blocking type doesn’t mean that the Wire is dead forever, but rather that for right now it should be noted as blocking.

= The blocking value. Wires can either produce a value to show that it’s functioning (the b type, listed below), or a value to show that it’s blocking. The blocking type doesn’t mean that the Wire is dead forever, but rather that for right now it should be noted as blocking. m = The operating monad. In some functions relating to netwire, the return types are contained within this monad. This allows the user to perform some actions within that monad whilst still functioning within the FRP network.

= The operating monad. In some functions relating to netwire, the return types are contained within this monad. This allows the user to perform some actions within that monad whilst still functioning within the FRP network. a = The input value. This can be used as an input to the network in addition to the step value. The step value should be measuring some sort of increment, while the input value should be something that would otherwise influence how the wire works.

= The input value. This can be used as an input to the network in addition to the step value. The step value should be measuring some sort of increment, while the input value should be something that would otherwise influence how the wire works. b = The result value. It’s sort of similar how in the IO monad its result is known as IO a where a represents a kind. Here, b represents the kind, and is within the FRP network.

The Practice

Alright, I’m sure that piss-poor explanation was almost helpful. It’s time to apply it. How do you represent a simple value – one that never changes? Well since Netwire uses an Applicative Interface (per its own description, if I remember correctly), it’s pretty easy:

import Control.Wire import FRP.Netwire -- You don't need to import Control.Applicative, because -- Netwire automatically re-exports it for you. simpleWire :: Monad m => b -> Wire s e m a b simpleWire = pure

Woah that was easy… and also completely useless. All that simpleWire will do when applied to a value is make a Wire that will always, no matter the input or step, be that value. Call simpleWire 5 ? Congratulations, now you have a (Monad m, Num b) => Wire s e m a b with its value being 5 forever.

Working With Time

Alright, wait a second. What if you want to do something else? Lets say… have a float that progressively gets higher and higher in value?

import Prelude hiding ((.)) -- To use (.) in the scope of Categories instead import Control.Wire import FRP.Netwire speed :: Float speed = 0.5 pos :: (HasTime t s, Monad m) => Wire s e m a Float pos = integral 0 . pure speed

Woah woah woah, what’s that integral function? integral :: (HasTime t s, Fractional a) => a -> Wire s e m a a . Alright… let’s think about it for a second. We know what Fractional is, at least we should. HasTime is pretty self explanitory; it represents a value that has a time. It can have any blocking value and Monad , but both of its input and its output have to be Fractional . While that does explain its type, it doesn’t explain what the function actually does. In mathematical terms: integral integrates the value with respect to time. It takes a starting value (in this case, 0), and delta speed. Internally, it works something like this:

Given the last state x , the delta time dt , and the speed dx .

, the delta time , and the speed . Return x + (dt * dx) (extra parens for clarity).

You don’t need to know how it really works interally just yet; knowing that it does work is satisfactory for now. But let’s take a step back for a second. We know what this stuff is doing conceptually, right? It’s making a value that increases with time. But how do we know?

Showing the User

Netwire provides a nice function named testWire :: (MonadIO m, Show b, Show e) => Session m s -> (forall a. Wire s e Identity a b) -> m c . Effectively what that means is so long as you provide a Session (we’ll get into it in a second), and both the e and b values can be converted into String s, it will constantly print out the value of that Wire . Lets try hooking that up.

import Prelude hiding ((.)) -- To use (.) in the scope of Categories instead import Control.Wire import FRP.Netwire speed :: Float speed = 0.5 -- We're changing the e value to () because it has a Show instance pos :: (HasTime t s, Monad m) => Wire s () m a Float pos = integral 0 . pure speed -- The main function main :: IO () main = testWire clockSession_ pos

If you try copying and running that, you’ll find a floating point number (starting at 0) slowly increase. ‘But how does it know!?’ you ask me, screaming in a manic craze. Slow down fella, I say, pointing to clockSession_ . That’s the secret, it’s a Session , as specified above. It automatically generates the time between the last update and the current one.

Reacting to Input

Now that we understand how to do basic things, such as making a point, and one that moves linearly in a direction, we can move on to bigger and better things. Like how one manages to react to input with Netwire. Luckily, because Netwire has the parameter to operate within a Monad , one can write Monadic code within the Netwire network. This is especially useful when you want to get input from an external source, such as… let’s say… the input API from GLFW.

import Graphics.UI.GLFW import Control.Wire import FRP.Netwire isKeyDown :: Enum k => k -> Wire s () IO a () isKeyDown k = mkGen_ $ \_ -> do s <- getKey k return $ case s of Press -> Right () Release -> Left ()

So… how does this work? Well if you’ve kept the types in mind, you’d find that we replaced e and b with () . That means we don’t care about what it returns. () is null, nothing, nada, the information is stored elsewhere: in whether or not the Wire blocks. If the Wire blocks then it means the key is not down, if it produces a non-blocking () then it means the key must be held down. By using the Alternative typeclass (specifically the <|> function) in conjunction with Category ’s (.) function, you can use this to define Wire s that react to input. It makes more sense if you look at an example:

import Prelude hiding ((.)) -- Hiding preludes (.) function, so Category's -- (.) function can take its place. import Graphics.UI.GLFW import Control.Wire import FRP.Netwire isKeyDown :: (Enum k, Monoid e) => k -> Wire s e IO a e isKeyDown k = mkGen_ $ \_ -> do s <- getKey k return $ case s of Press -> Right mempty Release -> Left mempty speed :: Monoid e => Wire s e IO a Float speed = pure ( 0.0) . isKeyDown (CharKey 'A') . isKeyDown (CharKey 'D') <|> pure (-0.5) . isKeyDown (CharKey 'A') <|> pure ( 0.5) . isKeyDown (CharKey 'D') <|> pure ( 0.0) pos :: HasTime t s => Wire s () IO a Float pos = integral 0 . speed

What’s happening here is, when any of the isKeyDown calls in the speed function block, Alternative takes over and moves to the next piece of code after the current <|> block. Unfortunately this can’t be as easily displayed with the testWire function. This is because it operates in the IO Monad and the testWire function requires something of the Identity monad. Until we hook it up to OpenGL and GLFW, I advise you to look at it in a more abstract logical sense:

<action> . isKeyDown <key> . isKeyDown <key2> . <etc> IS LOGICALLY EQUIVALENT TO Perform the <action> so long as the keys <key>, <key2>, and whatever other keys located in the <etc> block are held down.

Try to think about that for a little bit.

Hooking Things Up

Now we’ve covered working with Netwire a bit, it’s time to try to hook it up to OpenGL. This part, as noted in the very beginning, is in no way a tutorial on OpenGL. The techniques used are probably not very up-to-date, buy they sure as hell are simple. Lets go back to the beginning for a second:

import Graphics.Rendering.OpenGL import Graphics.UI.GLFW import Data.IORef main :: IO () main = do initialize openWindow (Size 640 480) [DisplayRGBBits 8 8 8, DisplayAlphaBits 8, DisplayDepthBits 24] Window closedRef <- newIORef False windowCloseCallback $= do writeIORef closedRef True return True -- Hook up to the Netwire network closeWindow

See that bit about “Hook up to the Netwire network”? That’s what we’re about to do. Let’s write a function to run the network (along with some other stuff to make it work):

s :: Float s = 0.05 y :: Float y = 0.0 renderPoint :: (Float, Float) -> IO () renderPoint (x, y) = vertex $ Vertex2 (realToFrac x :: GLfloat) (realToFrac y :: GLfloat) generatePoints :: Float -> Float -> Float -> [(Float, Float)] generatePoints x y s = [ (x - s, y - s) , (x + s, y - s) , (x + s, y + s) , (x - s, y + s) ] runNetwork :: (HasTime t s) => IORef Bool -> Session IO s -> Wire s e IO a Float -> IO () runNetwork closedRef session wire = do pollEvents closed <- readIORef closedRef if closed then return () else do (st , session') <- stepSession session (wt', wire' ) <- stepWire wire st $ Right undefined case wt' of Left _ -> return () Right x -> do clear [ColorBuffer] renderPrimitive Quads $ mapM_ renderPoint $ generatePoints x y s swapBuffers runNetwork closedRef session' wire'

Great, now lets go back up there, and put it in:

closedRef <- newIORef False windowCloseCallback $= do writeIORef closedRef True return True runNetwork closedRef clockSession_ pos closeWindow

You’ll find, at this point, that when you run the program you’ll have a tiny block in the middle of the screen that you can control by pressing A and D on your keyboard. At least you should. All of the source, as stated at the very beginning, can be found on GitHub. I guarantee that the code there built and ran fine on my machines (desktop running Windows 7, and laptop running Ubuntu).