From 9e60fbfff0e68460629a8560a50a418227a514a8 Mon Sep 17 00:00:00 2001 From: Dimitri Sokolyuk Date: Sun, 28 Aug 2016 14:53:42 +0200 Subject: Import react problem --- go/react/README.md | 32 ++++++ go/react/interfaces.go | 49 +++++++++ go/react/react_test.go | 278 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 go/react/README.md create mode 100644 go/react/interfaces.go create mode 100644 go/react/react_test.go 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") + } +} -- cgit v1.2.3