An FP game engine for Scala.

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

SignalFunctions 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)

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))
val makeRange: SignalFunction[Boolean, List[Int]] =
  SignalFunction { p =>
    val num = if (p) 10 else 5
    (1 to num).toList
  }
val chooseCatsOrDogs: SignalFunction[Boolean, String] =
  SignalFunction(p => if (p) "dog" else "cat")
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:

  1. &&& / and - run in parallel and tuple the results
  2. >>> / 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)