Blog Archive

Saturday, February 14, 2009

Functional Reactive Programming in F# (2)

In my previous post about Functional Reactive Programming, we have seen how a continuous function of time could be implemented in F# (Behaviors) and how it was possible to create more complex Behaviors by combining simpler Behaviors.
However, such Behaviors are in fact 'pure' functions of time, without any state.
In this post, we will see how Behaviors can handle state in a functional way (without side effects).
We start by considering a very simple state machine that consumes unit values () and produces a succession of boolean values: true, false, true, false...
Let's start with a classic, imperative implementation:

Here, a state machine that produces results of type 'a is simply represented as a function from unit to 'a.
Here is a second example.
The next machine accepts a unit option as input value and returns a result which is defined as follows:

If the input value is None then the machine returns -1.

If the input value is Some() then the machine returns the number of None received since the last Some().

In both examples above, we see that the machine needs to retain some state between two invocations and it does it with the help of some side effects (ref 0 and ref true).
There are several solutions to create state machines without side effects. The general idea is to design a machine that externalizes its state. In practice, that means:

Its state should be given as an additional input argument.

The machine produces a result that, in addition to itsexpected output, contains its new state.

Let's illustrate this with the previous example (the CountingMachine above).
First, we have to modify the type representing the machine. It is now a function that takes 2 arguments. The first one is the previous state of the machine (the state before the call) and the second is the usual unit option value. The state is simply the current count of None values.
This function returns a pair made of the count of None as explained above and the new state of the machine after the call.

We see clearly the drawbacks of such a solution. The runList function must be aware of two things:

The first, as already mentionned, is the handling of the state. Unfortunately, in this solution and in the next solution (a little bit below), this cannot be avoided.

The other drawback is the dependency of runList on the type of the state - int in this case. Thus, every Machine designed on state externalization needs its own runList function.

A third drawback, a bit less obvious at first sight, is the fact that the state must always be of the same type across several calls. It is not possible to replace, from one call to another, such a Machine with another that has a state of a different type.

Of course, the type for the machine presented above may be made generic:

Last solution, based on residual state machines.
This solution is based on a simple fact. Since a State Machine is basically a function of one argument, let's imagine that this function returns both its expected value and new State Machine. This a new State Machine acts a bit like the state in the previous example.
The type of such a machine is:

type 'a StateMachine = SM of (unit option -> ('a * 'a StateMachine))

We can already define some simple state machines:
A machine that always returns true. Two implementation are possible, depending on which definition is recursive.

This runList function is somewhat similar to the one already defined above but with 2 advantages:

It does not depend on the way the state machine models its state (or not - see the AlwaysTrueMachine which has no state at all).

As a consequence, a state machine may well change its state representation at will. The only condition is that the values it produces must have the same type.

So we come to the end of this post. As a last note, I must admit that there could be another way to model state machines, which is based on sequences but I have not explored this direction yet.
In a next post, I will make the connection between state machines and Behaviors.