I’m making a pause in the “semaphores” series to visit a problem from exercism.io, namely react in Go. As the description says, the idea is to implement a basic reactive system in Go.

Wikipedia defines “reactive programming” as “a programming paradigm oriented around data flows and the propagation of change. This means that it should be possible to express static or dynamic data flows with ease in the programming languages used, and that the underlying execution model will automatically propagate changes through the data flow.”

Basically, any modern spreadsheet program.

In the proposed exercise, the idea is to “implement a basic reactive system with cells with settable values (“input” cells) and cells with values computed in terms of other cells (“compute” cells). Implement updates so that when an input value is changed, values propagate to reach a new stable system state.”

This is exercise is the first one that (at least for me) requires a less-than-trivial amount of code to solve it. It’s also interesting because it forces you to deal with a Go feature that is often overlooked: interfaces. This is why I thought it would make for an interesting blog post.

As usual in exercism.io, a series of tests are provided, and in this case also some basic starting code.

Looking at the tests, the first one is very simple:

func TestSetInput(t *testing.T) {
	r := New()
	i := r.CreateInput(1)
	assertCellValue(t, i, 1, "i.Value() doesn't match initial value")
	i.SetValue(2)
	assertCellValue(t, i, 2, "i.Value() doesn't match changed value")
}

The function New returns an instance of “something” satisfying the interface Reactor, which looks like this:

// A Reactor manages linked cells.
type Reactor interface {
	// CreateInput creates an input cell linked into the reactor
	// with the given initial value.
	CreateInput(int) InputCell

	// CreateCompute1 creates a compute cell which computes its value
	// based on one other cell. The compute function will only be called
	// if the value of the passed cell changes.
	CreateCompute1(Cell, func(int) int) ComputeCell

	// CreateCompute2 is like CreateCompute1, but depending on two cells.
	// The compute function will only be called if the value of any of the
	// passed cells changes.
	CreateCompute2(Cell, Cell, func(int, int) int) ComputeCell
}

As you can see, a reactor basically creates several kinds of cells. The fact that the documentation says that it manages them is interesting, as it suggests a possible implementation that the person who developed the exercise had in mind.

If you examine the way New is used, the type returned by this function is never named explicitly in the tests. All that matters is that the interface is satisfied, in other words, that all the methods required by the interface are implemented.

My implementation of New looks like this:

func New() Reactor {
	return &reactor{}
}

where a pointer to reactor satisfies the Reactor interface. In order to pass the first test, CreateInput looks like this:

func (r *reactor) CreateInput(i int) InputCell {
	return &inputCell{cell: cell{v: i}}
}

why a pointer receiver if the receiver is not getting changed? Initially I though that reactor was going to manage the cells it created, as the documentation for the interface suggested, but since I followed a TDD approach to solving the exersice, eventually the implementation went in a different direction and this wasn’t necessary.

A pointer to inputCell satisfies the InputCell interface, and the type looks like this:

type inputCell struct {
	cell
}

It embeds another type cell because there some common parts shared with other two cell types. This mimics the InputCell interface:

// A Cell is conceptually a holder of a value.
type Cell interface {
	// Value returns the current value of the cell.
	Value() int
}

// An InputCell has a changeable value, changing the value triggers updates to
// other cells.
type InputCell interface {
	Cell

	// SetValue sets the value of the cell.
	SetValue(int)
}

InputCell embeds the Cell interface. My Initial inputCell implementation didn’t do this and instead satisfied the interface by implementing the methods directly. After some refactoring, cell stores the value of the cell and provides the Value() method. This will become important later. inputCell extends this by implementing the SetValue(int) method:

func (i *inputCell) SetValue(v int) {
	i.setValue(v)
}

where the setValue(int) method is actually provided by the cell type. With this it’s enough to pass the first test.

The other two methods of the Reactor interface (CreateCompute1 and CreateCompute2) follow a similar logic: create an instance of a type that implements the ComputeCell interface:

// A ComputeCell always computes its value based on other cells and can
// call callbacks upon changes.
type ComputeCell interface {
	Cell

	// AddCallback adds a callback which will be called when the value changes.
	// It returns a callback handle which can be used to remove the callback.
	AddCallback(func(int)) CallbackHandle

	// RemoveCallback removes a previously added callback, if it exists.
	RemoveCallback(CallbackHandle)
}

There are multiple underlying types, because they need to store different information. As the documentation says, the callback is called with the value changes. This means that the compute cell needs to known the its inputs change. I figured the simplest way to achieve that was to add the callback mechanism to the cell type, which is why it actually looks like this:

type cell struct {
	v         int
	callbacks map[int]func(int)
}

the cell stores callbacks to be called when the stored value changes. This is why cell has a setValue method:

func (c *cell) setValue(v int) int {
	if c.v != v {
		c.v = v
		for _, cb := range c.callbacks {
			cb(c.v)
		}
	}

	return c.v
}

This method checks that the new value is different from the current value, and if that’s the case, it stores the new value and calls the callbacks.

What this means is that when the value in the inputCell is changed, its callbacks are called. In this way, creating a computeCell1 looks like this:

func (r *reactor) CreateCompute1(c Cell, f func(int) int) ComputeCell {
	cc := &computeCell1{i: c, f: f}

	if a, ok := c.(addCallbacker); ok {
		a.addCallback(func(int) { cc.Value() })
	}

	// This forces one evaluation of the inputs
	cc.Value()

	return cc
}

here addCallbacker is an interface that requires the addCallback method to be implemented. All the actual types that satisfy the Cell interface also satisfy this other interface, so the type assertion is always going to work. It’s worth nothing that this is always possible to do: if you have a value passed using an interface, you can type-assert to a different interface to check that the actual type of the value satisfies the other interface.

The callback is written like that to adapt the signature of the existing method to that of the callbacks.

The other function (CreateCompute2) is very similar, with the only difference that is necessary to register two callbacks.

Why is the callback calling Value()? Because it made the implementation simple:

func (c *computeCell1) Value() int {
	return c.setValue(c.f(c.i.Value()))
}

func (c *computeCell2) Value() int {
	return c.setValue(c.f(c.i1.Value(), c.i2.Value()))
}

as you can see, the compute cells perform the computation when the Value() is queried and they store the newly computed value. If the value doesn’t change, setValue won’t change the stored value and the callbacks won’t be called. Since Value is called when the inputs change, this causes the compute cells to propage the changes as they happen.

If you look closely, this implementation is far from optimal, because the computations might be done multiple times for a single change. For example, if you have i -> c1 -> c2, changing the value in i will call Value in c1, which triggers the call to Value in c2, which in turn calls Value in c1, which doesn’t call the callbacks again because that second time the value doesn’t change (but you can pass a function to the compute cell that causes this process to enter a loop).

Nevertheless I found it interesting that the problem has a relatively simple solution that uses very straightforward constructions.

Next week is back to the semaphores series.