Signals & Signal Functions
Motivation
We want pure, referentially transparent, testable, procedural values that change over time, AKA, 'animations' (but we can 'animate' any value).
Background
The goal of Indigo is to make programming games (as opposed to making them...), easier to reason about and easier to test by leveraging the good ideas that come with functional programming.
One of those good ideas that Indigo borrows is the notion of Signals and Signal Functions. Signals for animation were first proposed in the Functional Reactive ANimation (FRAN) system, but are most readily seen in Yampa.
Indigo makes use of Signals too, though not to the same extent as FRAN or Yampa. The main difference is that Signals in Indigo are stateless and therefore somewhat limited. Nonetheless, Signals in Indigo are still interesting and useful, and provide a core building block for other systems in Indigo.
Signals
At it's core, a signal is a very simple thing. Consider this function signature:
val f: A => B
What this function signature says is that when provided some value of type A
, it will produce some value of type B
. In concrete terms, if we fix the types to known primitives, the follow example says that when given a String
, it will return an Int
:
val f: String => Int
A Signal
of A
is nothing more than a function that when given the current running time in Seconds
, will produce some value of type A
.
val f: Seconds => A
Signal Functions
SignalFunction
s allow you to compose functions that operate on signals.
Signal functions are combinators, and a combinator is a function that takes a function as an argument and returns another function, like this:
// A function that takes as an argument a function that takes an
// Int and returns a String, and then returns a function that
// takes and Int and returns a Boolean
val f: (Int => String) => (Int => Boolean)
A Signal function takes a Signal[A]
and returns a Signal[B]
.
SignalFunction(f: Signal[A] => Signal[B])
which is really a combinator:
val f: (Seconds => A) => (Seconds => B)
Example Links
Signals in Action
Let's set up a contrived set of signal functions and println the results. In our example depending on the time, we're going to make a list of cats or a list of dogs, and vary the length of the list. This is not a sensible example, and clearly you could do this in other ways, but it's a nice way to illustrate how signals and signal functions work in the familar terriroty of making lists.
Signals for animation will be explored in the guides.
Please note that this demo does not show anything visual on the screen, please see the JS console in your browser for the output.
Pulse is a type of signal. Based on the time, it will produce an on/off boolean like:
// ____ ____ ____
// ___| |___| |___| |___
val signal = Signal.Pulse(Seconds(1))
- Taking our 'pulse' boolean value, we can choose a range of 1 to 5, or 1 to 10.
val makeRange: SignalFunction[Boolean, List[Int]] =
SignalFunction { p =>
val num = if (p) 10 else 5
(1 to num).toList
}
- Using the same boolean, we can also decide if we're talking about cats or dogs.
val chooseCatsOrDogs: SignalFunction[Boolean, String] =
SignalFunction(p => if (p) "dog" else "cat")
- Given our lists of numbers and our cats and dogs, we need a way to make the final result.
val howManyPets: SignalFunction[(List[Int], String), List[String]] =
SignalFunction { case (l, str) =>
l.map(_.toString + " " + str)
}
Finally, we can compose our functions together using a couple of operators:
&&&
/and
- run in parallel and tuple the results>>>
/andThen
- compose the functions together from left to right
val signalFunction = (makeRange &&& chooseCatsOrDogs) >>> howManyPets
To run the examples, we'll pipe the pulse signal into our signal function using the pipe |>
operator.
def updateModel(
context: Context[Unit],
hasRunOnce: Boolean
): GlobalEvent => Outcome[Boolean] =
case FrameTick if !hasRunOnce =>
val allTheDogs = (signal |> signalFunction).at(Seconds.zero)
println(allTheDogs)
// List("1 dog", "2 dog", "3 dog", "4 dog", "5 dog", "6 dog", "7 dog", "8 dog", "9 dog", "10 dog")
val allTheCats = (signal |> signalFunction).at(Seconds(1))
println(allTheCats)
// List("1 cat", "2 cat", "3 cat", "4 cat", "5 cat")
Outcome(true)
case _ =>
Outcome(hasRunOnce)