Initial Commit
This commit is contained in:
32
go/react/README.md
Normal file
32
go/react/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# React
|
||||
|
||||
Implement a basic reactive system.
|
||||
|
||||
Reactive programming is a programming paradigm that focuses on how values
|
||||
are computed in terms of each other to allow a change to one value to
|
||||
automatically propagate to other values, like in a spreadsheet.
|
||||
|
||||
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.
|
||||
|
||||
In addition, compute cells should allow for registering change notification
|
||||
callbacks. Call a cell’s callbacks when the cell’s value in a new stable
|
||||
state has changed from the previous stable state.
|
||||
|
||||
To run the tests simply run the command `go test` in the exercise directory.
|
||||
|
||||
If the test suite contains benchmarks, you can run these with the `-bench`
|
||||
flag:
|
||||
|
||||
go test -bench .
|
||||
|
||||
For more detailed info about the Go track see the [help
|
||||
page](http://exercism.io/languages/go).
|
||||
|
||||
|
||||
|
||||
## Submitting Incomplete Problems
|
||||
It's possible to submit an incomplete solution so you can see how others have completed the exercise.
|
||||
|
49
go/react/interfaces.go
Normal file
49
go/react/interfaces.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package react
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// A CallbackHandle is used to remove previously added callbacks, see ComputeCell.
|
||||
type CallbackHandle interface{}
|
94
go/react/react.go
Normal file
94
go/react/react.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package react
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const testVersion = 4
|
||||
|
||||
// MyReactor implements Reactor
|
||||
type MyReactor struct {
|
||||
lastId int
|
||||
cells []Cell
|
||||
}
|
||||
|
||||
// New creates a new Reactor
|
||||
func New() *MyReactor {
|
||||
r := &MyReactor{}
|
||||
//r.callbacks = make(map[Cell][]func(int))
|
||||
return r
|
||||
}
|
||||
|
||||
// CreateInput builds an input cell and adds it to the reactor
|
||||
func (r *MyReactor) CreateInput(i int) InputCell {
|
||||
r.lastId++
|
||||
ic := MyCell{val: i, id: r.lastId}
|
||||
return &ic
|
||||
}
|
||||
|
||||
// CreateCompute1 Takes a cell and a function and returns a compute cell
|
||||
// which has a value based on running the function on the cells value.
|
||||
func (r *MyReactor) CreateCompute1(c Cell, f func(int) int) ComputeCell {
|
||||
r.lastId++
|
||||
cc := &MyCell{id: r.lastId, isComputed: true}
|
||||
cc.compVal = func() int { return f(c.Value()) }
|
||||
return cc
|
||||
}
|
||||
|
||||
// CreateCompute2 Takes two cells and a function and returns a compute cell
|
||||
// which has a value based on running the function on the cells values.
|
||||
func (r *MyReactor) CreateCompute2(c1, c2 Cell, f func(int, int) int) ComputeCell {
|
||||
r.lastId++
|
||||
cc := &MyCell{id: r.lastId, isComputed: true}
|
||||
cc.compVal = func() int { return f(c1.Value(), c2.Value()) }
|
||||
return cc
|
||||
}
|
||||
|
||||
// MyCell implements the all Cell interfaces
|
||||
type MyCell struct {
|
||||
id int
|
||||
isComputed bool
|
||||
val int
|
||||
compVal func() int
|
||||
lastCallbackId int
|
||||
callbacks map[int]func(int)
|
||||
}
|
||||
|
||||
// Value returns the value of the cell
|
||||
func (c MyCell) Value() int {
|
||||
if c.isComputed {
|
||||
return c.compVal()
|
||||
}
|
||||
return c.val
|
||||
}
|
||||
|
||||
// SetValue sets the value on the cell
|
||||
func (c *MyCell) SetValue(i int) {
|
||||
if i == c.val || c.isComputed {
|
||||
// No change or this is a computed cell, just return
|
||||
return
|
||||
}
|
||||
c.val = i
|
||||
// Hit all callbacks
|
||||
for _, v := range c.callbacks {
|
||||
fmt.Println("Hitting a callback: " + strconv.Itoa(i))
|
||||
v(i)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MyCell) AddCallback(cb func(int)) CallbackHandle {
|
||||
if c.lastCallbackId == 0 {
|
||||
fmt.Println("Initializing Callback Map (Cell " + strconv.Itoa(c.id) + ")")
|
||||
c.callbacks = make(map[int]func(int))
|
||||
}
|
||||
fmt.Println("Adding a Callback to " + strconv.Itoa(c.id))
|
||||
c.lastCallbackId++
|
||||
c.callbacks[c.lastCallbackId] = cb
|
||||
fmt.Println("Number of Callbacks: " + strconv.Itoa(c.lastCallbackId))
|
||||
return c.lastCallbackId
|
||||
}
|
||||
|
||||
func (c *MyCell) RemoveCallback(cbh CallbackHandle) {
|
||||
delete(c.callbacks, cbh.(int))
|
||||
}
|
278
go/react/react_test.go
Normal file
278
go/react/react_test.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package react
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Define a function New() Reactor and the stuff that follows from
|
||||
// implementing Reactor.
|
||||
//
|
||||
// Also define a testVersion with a value that matches
|
||||
// the targetTestVersion here.
|
||||
|
||||
const targetTestVersion = 4
|
||||
|
||||
// This is a compile time check to see if you've properly implemented New().
|
||||
var _ Reactor = New()
|
||||
|
||||
// If this test fails and you've proprly defined testVersion the requirements
|
||||
// of the tests have changed since you wrote your submission.
|
||||
func TestTestVersion(t *testing.T) {
|
||||
if testVersion != targetTestVersion {
|
||||
t.Fatalf("Found testVersion = %v, want %v", testVersion, targetTestVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func assertCellValue(t *testing.T, c Cell, expected int, explanation string) {
|
||||
observed := c.Value()
|
||||
_, _, line, _ := runtime.Caller(1)
|
||||
if observed != expected {
|
||||
t.Fatalf("(from line %d) %s: expected %d, got %d", line, explanation, expected, observed)
|
||||
}
|
||||
}
|
||||
|
||||
// Setting the value of an input cell changes the observable Value()
|
||||
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 value of a compute 1 cell is determined by the value of the dependencies.
|
||||
func TestBasicCompute1(t *testing.T) {
|
||||
r := New()
|
||||
i := r.CreateInput(1)
|
||||
c := r.CreateCompute1(i, func(v int) int { return v + 1 })
|
||||
assertCellValue(t, c, 2, "c.Value() isn't properly computed based on initial input cell value")
|
||||
i.SetValue(2)
|
||||
assertCellValue(t, c, 3, "c.Value() isn't properly computed based on changed input cell value")
|
||||
}
|
||||
|
||||
// The value of a compute 2 cell is determined by the value of the dependencies.
|
||||
func TestBasicCompute2(t *testing.T) {
|
||||
r := New()
|
||||
i1 := r.CreateInput(1)
|
||||
i2 := r.CreateInput(2)
|
||||
c := r.CreateCompute2(i1, i2, func(v1, v2 int) int { return v1 | v2 })
|
||||
assertCellValue(t, c, 3, "c.Value() isn't properly computed based on initial input cell values")
|
||||
i1.SetValue(4)
|
||||
assertCellValue(t, c, 6, "c.Value() isn't properly computed when first input cell value changes")
|
||||
i2.SetValue(8)
|
||||
assertCellValue(t, c, 12, "c.Value() isn't properly computed when second input cell value changes")
|
||||
}
|
||||
|
||||
// Compute 2 cells can depend on compute 1 cells.
|
||||
func TestCompute2Diamond(t *testing.T) {
|
||||
r := New()
|
||||
i := r.CreateInput(1)
|
||||
c1 := r.CreateCompute1(i, func(v int) int { return v + 1 })
|
||||
c2 := r.CreateCompute1(i, func(v int) int { return v - 1 })
|
||||
c3 := r.CreateCompute2(c1, c2, func(v1, v2 int) int { return v1 * v2 })
|
||||
assertCellValue(t, c3, 0, "c3.Value() isn't properly computed based on initial input cell value")
|
||||
i.SetValue(3)
|
||||
assertCellValue(t, c3, 8, "c3.Value() isn't properly computed based on changed input cell value")
|
||||
}
|
||||
|
||||
// Compute 1 cells can depend on other compute 1 cells.
|
||||
func TestCompute1Chain(t *testing.T) {
|
||||
r := New()
|
||||
inp := r.CreateInput(1)
|
||||
var c Cell = inp
|
||||
for i := 2; i <= 8; i++ {
|
||||
// must save current value of loop variable i for correct behavior.
|
||||
// compute function has to use digitToAdd not i.
|
||||
digitToAdd := i
|
||||
c = r.CreateCompute1(c, func(v int) int { return v*10 + digitToAdd })
|
||||
}
|
||||
assertCellValue(t, c, 12345678, "c.Value() isn't properly computed based on initial input cell value")
|
||||
inp.SetValue(9)
|
||||
assertCellValue(t, c, 92345678, "c.Value() isn't properly computed based on changed input cell value")
|
||||
}
|
||||
|
||||
// Compute 2 cells can depend on other compute 2 cells.
|
||||
func TestCompute2Tree(t *testing.T) {
|
||||
r := New()
|
||||
ins := make([]InputCell, 3)
|
||||
for i, v := range []int{1, 10, 100} {
|
||||
ins[i] = r.CreateInput(v)
|
||||
}
|
||||
|
||||
add := func(v1, v2 int) int { return v1 + v2 }
|
||||
|
||||
firstLevel := make([]ComputeCell, 2)
|
||||
for i := 0; i < 2; i++ {
|
||||
firstLevel[i] = r.CreateCompute2(ins[i], ins[i+1], add)
|
||||
}
|
||||
|
||||
output := r.CreateCompute2(firstLevel[0], firstLevel[1], add)
|
||||
assertCellValue(t, output, 121, "output.Value() isn't properly computed based on initial input cell values")
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
ins[i].SetValue(ins[i].Value() * 2)
|
||||
}
|
||||
|
||||
assertCellValue(t, output, 242, "output.Value() isn't properly computed based on changed input cell values")
|
||||
}
|
||||
|
||||
// Compute cells can have callbacks.
|
||||
func TestBasicCallback(t *testing.T) {
|
||||
r := New()
|
||||
i := r.CreateInput(1)
|
||||
c := r.CreateCompute1(i, func(v int) int { return v + 1 })
|
||||
var observed []int
|
||||
c.AddCallback(func(v int) {
|
||||
observed = append(observed, v)
|
||||
})
|
||||
if len(observed) != 0 {
|
||||
t.Fatalf("callback called before changes were made")
|
||||
}
|
||||
i.SetValue(2)
|
||||
if len(observed) != 1 {
|
||||
t.Fatalf("callback not called when changes were made")
|
||||
}
|
||||
if observed[0] != 3 {
|
||||
t.Fatalf("callback not called with proper value")
|
||||
}
|
||||
}
|
||||
|
||||
// Callbacks and only trigger on change.
|
||||
func TestOnlyCallOnChanges(t *testing.T) {
|
||||
r := New()
|
||||
i := r.CreateInput(1)
|
||||
c := r.CreateCompute1(i, func(v int) int {
|
||||
if v > 3 {
|
||||
return v + 1
|
||||
}
|
||||
return 2
|
||||
})
|
||||
var observedCalled int
|
||||
c.AddCallback(func(int) {
|
||||
observedCalled++
|
||||
})
|
||||
i.SetValue(1)
|
||||
if observedCalled != 0 {
|
||||
t.Fatalf("observe function called even though input didn't change")
|
||||
}
|
||||
i.SetValue(2)
|
||||
if observedCalled != 0 {
|
||||
t.Fatalf("observe function called even though computed value didn't change")
|
||||
}
|
||||
}
|
||||
|
||||
// Callbacks can be added and removed.
|
||||
func TestCallbackAddRemove(t *testing.T) {
|
||||
r := New()
|
||||
i := r.CreateInput(1)
|
||||
c := r.CreateCompute1(i, func(v int) int { return v + 1 })
|
||||
var observed1 []int
|
||||
cb1 := c.AddCallback(func(v int) {
|
||||
observed1 = append(observed1, v)
|
||||
})
|
||||
var observed2 []int
|
||||
c.AddCallback(func(v int) {
|
||||
observed2 = append(observed2, v)
|
||||
})
|
||||
i.SetValue(2)
|
||||
if len(observed1) != 1 || observed1[0] != 3 {
|
||||
t.Fatalf("observed1 not properly called")
|
||||
}
|
||||
if len(observed2) != 1 || observed2[0] != 3 {
|
||||
t.Fatalf("observed2 not properly called")
|
||||
}
|
||||
c.RemoveCallback(cb1)
|
||||
i.SetValue(3)
|
||||
if len(observed1) != 1 {
|
||||
t.Fatalf("observed1 called after removal")
|
||||
}
|
||||
if len(observed2) != 2 || observed2[1] != 4 {
|
||||
t.Fatalf("observed2 not properly called after first callback removal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleCallbackRemoval(t *testing.T) {
|
||||
r := New()
|
||||
inp := r.CreateInput(1)
|
||||
c := r.CreateCompute1(inp, func(v int) int { return v + 1 })
|
||||
|
||||
numCallbacks := 5
|
||||
|
||||
calls := make([]int, numCallbacks)
|
||||
handles := make([]CallbackHandle, numCallbacks)
|
||||
for i := 0; i < numCallbacks; i++ {
|
||||
// Rebind i, otherwise all callbacks will use i = numCallbacks
|
||||
i := i
|
||||
handles[i] = c.AddCallback(func(v int) { calls[i]++ })
|
||||
}
|
||||
|
||||
inp.SetValue(2)
|
||||
for i := 0; i < numCallbacks; i++ {
|
||||
if calls[i] != 1 {
|
||||
t.Fatalf("callback %d/%d should be called 1 time, was called %d times", i+1, numCallbacks, calls[i])
|
||||
}
|
||||
c.RemoveCallback(handles[i])
|
||||
}
|
||||
|
||||
inp.SetValue(3)
|
||||
for i := 0; i < numCallbacks; i++ {
|
||||
if calls[i] != 1 {
|
||||
t.Fatalf("callback %d/%d was called after it was removed", i+1, numCallbacks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveIdempotence(t *testing.T) {
|
||||
r := New()
|
||||
inp := r.CreateInput(1)
|
||||
output := r.CreateCompute1(inp, func(v int) int { return v + 1 })
|
||||
timesCalled := 0
|
||||
cb1 := output.AddCallback(func(int) {})
|
||||
output.AddCallback(func(int) { timesCalled++ })
|
||||
for i := 0; i < 10; i++ {
|
||||
output.RemoveCallback(cb1)
|
||||
}
|
||||
inp.SetValue(2)
|
||||
if timesCalled != 1 {
|
||||
t.Fatalf("remaining callback function was not called")
|
||||
}
|
||||
}
|
||||
|
||||
// Callbacks should only be called once even though
|
||||
// multiple dependencies have changed.
|
||||
func TestOnlyCallOnceOnMultipleDepChanges(t *testing.T) {
|
||||
r := New()
|
||||
i := r.CreateInput(1)
|
||||
c1 := r.CreateCompute1(i, func(v int) int { return v + 1 })
|
||||
c2 := r.CreateCompute1(i, func(v int) int { return v - 1 })
|
||||
c3 := r.CreateCompute1(c2, func(v int) int { return v - 1 })
|
||||
c4 := r.CreateCompute2(c1, c3, func(v1, v3 int) int { return v1 * v3 })
|
||||
changed4 := 0
|
||||
c4.AddCallback(func(int) { changed4++ })
|
||||
i.SetValue(3)
|
||||
if changed4 < 1 {
|
||||
t.Fatalf("callback function was not called")
|
||||
} else if changed4 > 1 {
|
||||
t.Fatalf("callback function was called too often")
|
||||
}
|
||||
}
|
||||
|
||||
// Callbacks should not be called if dependencies change in such a way
|
||||
// that the final value of the compute cell does not change.
|
||||
func TestNoCallOnDepChangesResultingInNoChange(t *testing.T) {
|
||||
r := New()
|
||||
inp := r.CreateInput(0)
|
||||
plus1 := r.CreateCompute1(inp, func(v int) int { return v + 1 })
|
||||
minus1 := r.CreateCompute1(inp, func(v int) int { return v - 1 })
|
||||
// The output's value is always 2, no matter what the input is.
|
||||
output := r.CreateCompute2(plus1, minus1, func(v1, v2 int) int { return v1 - v2 })
|
||||
|
||||
timesCalled := 0
|
||||
output.AddCallback(func(int) { timesCalled++ })
|
||||
|
||||
inp.SetValue(5)
|
||||
if timesCalled != 0 {
|
||||
t.Fatalf("callback function called even though computed value didn't change")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user