What is the basic structure in Netwire 5?
Asked Answered
R

2

8

I'm trying to get into Netwire, I've dug to find documentations, introductions, tutorials and whatnot, but just about every tutorial & existing-code is outdated as to Netwire 5 and uses functions from Netwire 4 that are just no longer with us. The README is kind of helpful, but not everything compiles and still it barely provides enough information to get started.

I am asking for explanation or an example just to get a game-loop running and being able to respond to events, so I seek information so I'll eventually know:

  1. The basic structure (like how in reactive-banana you actuate network-descriptions that consume handlers, define behaviors and reactimate to events).
  2. How it ultimately goes in main.
  3. How to handle IO events (like a mouse-click, a key-down or a game-loop callback), how do events come in sessions, etc.

And anything else relevant.

I figure that from there I could get something running, and so I could learn the rest by experimentation (as the state of documentation and tutorials for this in the 5th version is horribly non-existent, I hope some will appear very shortly).

Thank you!

Rao answered 16/8, 2014 at 9:36 Comment(0)
C
10

Disclaimer: I haven't been able to find any large-scale programs that use Netwire, so everything I'm about to write you should take with a grain of salt, as it's based on my own experiences using Netwire. The examples that I use here are mostly taken from my own library and attempt at writing a game using FRP, and might not be "the right way" to do things.


Question 1: The basic structure (like how in reactive-banana you actuate network-descriptions that consume handlers, define behaviors and reactimate to events).

Sessions: The author of the netwire library gave a really good answer about the basic structure of a netwire program. Since it's a bit old, I will outline some of the points here. Before we look at wires, let's first take a look at how netwire handles time, the underlying driver of FRP. The only way to advance time without using the testing harness testWire is to produce a Session that will statefully return time deltas. The way Sessions preserve state is encapsulated in their type:

newtype Session m s = Session { stepSession :: m (s, Session m s) }

Here, a Session lies within a Monad (usually IO) and every time it is evaluated, returns a "time state" value of type s and a new Session. Usually, any useful state s can be written as a Timed value that can return some instance of Real t:

data Timed t s
class (Monoid s, Real t) => HasTime t s | s -> t where
    -- | Extract the current time delta.
    dtime :: s -> t
instance (Monoid s, Real t) => HasTime t (Timed t s)

For example, in games, you usually want a fixed timestep to do your update calls. netwire encodes this notion with:

countSession_ :: Applicative m => t -> Session m (Timed t ())

A countSession_ takes as input the timestep, in this case a fixed value of type t, and produces a Session whose state values are of type Timed t (). This means that they only encode a single value of type t, and do not carry any additional state with the () value. After we discuss wires, we will see how this plays a role in evaluating them.

Wires: The main type of a 'wire' in Netwire is:

Wire s e m a b

This wire describes a reactive value of type b that does the following:

  • Takes as input a reactive value of type a
  • Operates within the Monad m
  • May inhibit, or not produce a value, yielding an inhibition value of type e
  • Assumes a time state given by s

By the nature of being reactive values, wires can be thought of as time-varying functions. Hence, each wire is encoded as a function of time (or the time state s) that produces, at that instant in time, a new value of type b, and a new wire with which to evaluate the next input of type a. By returning a value and a new wire, the function can encompass state by propagating it through the function definitions.

Additionally, wires may inhibit or not produce a value. This is useful for when computations are not defined (such as when the mouse is outside of the application window). This allows you to implement things like switch, where a wire changes to a different wire to continue execution (such as a player finishing his jump).

With these ideas, we can see the main driver of wires in netwire:

stepWire :: Monad m => Wire s e m a b -> s -> Either e a -> m (Either e b, Wire s e m a b)

stepWire wire timestate input does exactly what we said earlier: It takes a wire and passes it the current timestate and input from the previous wire. Then, in the underlying Monad m, it either produces a value of Right b or inhibits with a value of Left e, and then gives the next wire to use for the computation.

Question 2: How it ultimately goes in main.

Armed with values of type Session and Wire, we can construct a loop that does two things over and over:

  1. Steps a session to receive a new time state
  2. Uses the new time state to step a wire

Here is an example of a program that alters a fixed counter to count by twos forever:

import Control.Wire

-- My countLoop operates in the IO monad and takes two parameters:
--   1. A session that returns time states of type (Timed Int ())
--   2. A wire that ignores its input and returns an Int
countLoop :: Session IO (Timed Int ()) -> Wire (Timed Int ()) () IO a Int -> IO ()
countLoop session wire = do
  (st, nextSession) <- stepSession session
  (Right count, nextWire) <- stepWire wire st (Right undefined)
  print count
  countLoop nextSession nextWire

-- Main just initializes the procedure:
main :: IO ()
main = countLoop (countSession_ 1) $ time >>> (mkSF_ (*2))

Question 3: How to handle IO events (like a mouse-click, a key-down or a game-loop callback), how do events come in sessions, etc.

There is somewhat of a debate about how to do this. I think in this situation it's best to take advantage of the underlying Monad m, and simply pass a snapshot of the current state to the stepWire function. In doing so, most of my input wires look something like this:

mousePos :: Wire s e (State Input) a (Float, Float)

Where the input to the wire is ignored, and the mouse input is read from a State monad. I use State and not Reader in order to handle key debounces properly (so that clicking on UI doesn't also click on something underneath UI). The state is set in my main function and passed to runState, which also does the wire stepping. The inhibition behavior of wires like this can make for some elegant code. For example, suppose you have wires right and left for your arrow keys that produce a value if the key is pressed and inhibit otherwise. You can create character movement with a wire that looks like this:

(right >>> moveRight) <|> (left >>> moveLeft) <|> stayPut

Since wires are an instance of Alternative, if right inhibits, it'll just move on to the next possible wire. a <|> b will inhibit only if both a and b inhibit.

You could also write your code to take advantage of netwire's Event system, but you'd have to make your own wires that return an Event using Control.Wire.Unsafe.Event. That being said, I have yet to find this abstraction more useful than simple inhibition.

Competitive answered 16/8, 2014 at 12:42 Comment(1)
if we ever meet I'm going to buy you a beer for writing this answerEdie
B
0

It might be really late to answer this, but I have an answer here that contains the (minimalistic) structure of Netwire 5 program: Console interactivity in Netwire?

Barozzi answered 29/9, 2015 at 22:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.