summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDimitri Sokolyuk <demon@dim13.org>2016-08-28 14:53:42 +0200
committerDimitri Sokolyuk <demon@dim13.org>2016-08-28 14:53:42 +0200
commit9e60fbfff0e68460629a8560a50a418227a514a8 (patch)
treed56ffa08e886d483d0ab0186e7e07caf69ff6cb4
parenta9ae4ebc6006271fc0e95626e0c95e6b9cc786de (diff)
Import react problem
-rw-r--r--go/react/README.md32
-rw-r--r--go/react/interfaces.go49
-rw-r--r--go/react/react_test.go278
3 files changed, 359 insertions, 0 deletions
diff --git a/go/react/README.md b/go/react/README.md
new file mode 100644
index 0000000..e4f7d1a
--- /dev/null
+++ b/go/react/README.md
@@ -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.
+
diff --git a/go/react/interfaces.go b/go/react/interfaces.go
new file mode 100644
index 0000000..05f3e42
--- /dev/null
+++ b/go/react/interfaces.go
@@ -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{}
diff --git a/go/react/react_test.go b/go/react/react_test.go
new file mode 100644
index 0000000..0cc5f25
--- /dev/null
+++ b/go/react/react_test.go
@@ -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")
+ }
+}