Skip to content

Commit cab34fe

Browse files
authored
Merge pull request #29 from petertseng/react
react: add to track
2 parents eba60f4 + 58f3b19 commit cab34fe

File tree

6 files changed

+332
-0
lines changed

6 files changed

+332
-0
lines changed

config.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,16 @@
222222
"difficulty": 1,
223223
"slug": "change",
224224
"topics": []
225+
},
226+
{
227+
"difficulty": 10,
228+
"slug": "react",
229+
"topics": [
230+
"Reactive programming",
231+
"Generics",
232+
"Nested classes",
233+
"Higher-order functions"
234+
]
225235
}
226236
]
227237
}

exercises/react/build.gradle

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
buildscript {
2+
ext.kotlin_version = '1.0.6'
3+
repositories {
4+
mavenCentral()
5+
}
6+
dependencies {
7+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
8+
}
9+
}
10+
11+
apply plugin: 'kotlin'
12+
13+
repositories {
14+
mavenCentral()
15+
}
16+
17+
dependencies {
18+
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
19+
20+
testCompile 'junit:junit:4.12'
21+
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
22+
}
23+
test {
24+
testLogging {
25+
exceptionFormat = 'full'
26+
events = ["passed", "failed", "skipped"]
27+
}
28+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
class Reactor<T>() {
2+
abstract inner class Cell {
3+
abstract val value: T
4+
internal val dependents = mutableListOf<ComputeCell>()
5+
}
6+
7+
interface Subscription {
8+
fun cancel()
9+
}
10+
11+
inner class InputCell(initialValue: T) : Cell() {
12+
override var value: T = initialValue
13+
set(newValue) {
14+
field = newValue
15+
dependents.forEach { it.propagate() }
16+
dependents.forEach { it.fireCallbacks() }
17+
}
18+
}
19+
20+
inner class ComputeCell private constructor(val newValue: () -> T) : Cell() {
21+
override var value: T = newValue()
22+
private set
23+
24+
private var lastCallbackValue = value
25+
private var callbacksIssued = 0
26+
private val activeCallbacks = mutableMapOf<Int, (T) -> Any>()
27+
28+
constructor(vararg cells: Cell, f: (List<T>) -> T) : this({ f(cells.map { it.value }) }) {
29+
for (cell in cells) {
30+
cell.dependents.add(this)
31+
}
32+
}
33+
34+
fun addCallback(f: (T) -> Any): Subscription {
35+
val id = callbacksIssued
36+
callbacksIssued++
37+
activeCallbacks[id] = f
38+
return object : Subscription {
39+
override fun cancel() {
40+
activeCallbacks.remove(id)
41+
}
42+
}
43+
}
44+
45+
internal fun propagate() {
46+
val nv = newValue()
47+
if (nv == value) {
48+
return
49+
}
50+
value = nv
51+
dependents.forEach { it.propagate() }
52+
}
53+
54+
internal fun fireCallbacks() {
55+
if (value == lastCallbackValue) {
56+
return
57+
}
58+
lastCallbackValue = value
59+
for (cb in activeCallbacks.values) {
60+
cb(value)
61+
}
62+
dependents.forEach { it.fireCallbacks() }
63+
}
64+
}
65+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class Reactor<T>() {
2+
// Your compute cell's addCallback method must return an object
3+
// that implements the Subscription interface.
4+
interface Subscription {
5+
fun cancel()
6+
}
7+
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import org.junit.Test
2+
import org.junit.Ignore
3+
import org.junit.runner.RunWith
4+
import org.junit.runners.Parameterized
5+
import kotlin.test.assertEquals
6+
7+
class ReactTest {
8+
@Test
9+
fun inputCellsHaveValue() {
10+
val reactor = Reactor<Int>()
11+
val input = reactor.InputCell(10)
12+
assertEquals(10, input.value)
13+
}
14+
15+
@Ignore
16+
@Test
17+
fun inputCellsValueCanBeSet() {
18+
val reactor = Reactor<Int>()
19+
val input = reactor.InputCell(4)
20+
input.value = 20
21+
assertEquals(20, input.value)
22+
}
23+
24+
@Ignore
25+
@Test
26+
fun computeCellsCalculateInitialValue() {
27+
val reactor = Reactor<Int>()
28+
val input = reactor.InputCell(1)
29+
val output = reactor.ComputeCell(input) { it[0] + 1 }
30+
assertEquals(2, output.value)
31+
}
32+
33+
@Ignore
34+
@Test
35+
fun computeCellsTakeInputsInTheRightOrder() {
36+
val reactor = Reactor<Int>()
37+
val one = reactor.InputCell(1)
38+
val two = reactor.InputCell(2)
39+
val output = reactor.ComputeCell(one, two) { it[0] + it[1] * 10 }
40+
assertEquals(21, output.value)
41+
}
42+
43+
@Ignore
44+
@Test
45+
fun computeCellsUpdateValueWhenDependenciesAreChanged() {
46+
val reactor = Reactor<Int>()
47+
val input = reactor.InputCell(1)
48+
val output = reactor.ComputeCell(input) { it[0] + 1 }
49+
input.value = 3
50+
assertEquals(4, output.value)
51+
}
52+
53+
@Ignore
54+
@Test
55+
fun computeCellsCanDependOnOtherComputeCells() {
56+
val reactor = Reactor<Int>()
57+
val input = reactor.InputCell(1)
58+
val timesTwo = reactor.ComputeCell(input) { it[0] * 2 }
59+
val timesThirty = reactor.ComputeCell(input) { it[0] * 30 }
60+
val output = reactor.ComputeCell(timesTwo, timesThirty) { it[0] + it[1] }
61+
62+
assertEquals(32, output.value)
63+
input.value = 3
64+
assertEquals(96, output.value)
65+
}
66+
67+
@Ignore
68+
@Test
69+
fun computeCellsFireCallbacks() {
70+
val reactor = Reactor<Int>()
71+
val input = reactor.InputCell(1)
72+
val output = reactor.ComputeCell(input) { it[0] + 1 }
73+
74+
val vals = mutableListOf<Int>()
75+
output.addCallback { vals.add(it) }
76+
77+
input.value = 3
78+
assertEquals(listOf(4), vals)
79+
}
80+
81+
@Ignore
82+
@Test
83+
fun callbacksOnlyFireOnChange() {
84+
val reactor = Reactor<Int>()
85+
val input = reactor.InputCell(1)
86+
val output = reactor.ComputeCell(input) { if (it[0] < 3) 111 else 222 }
87+
88+
val vals = mutableListOf<Int>()
89+
output.addCallback { vals.add(it) }
90+
91+
input.value = 2
92+
assertEquals(listOf<Int>(), vals)
93+
94+
input.value = 4
95+
assertEquals(listOf(222), vals)
96+
}
97+
98+
@Ignore
99+
@Test
100+
fun callbacksCanBeAddedAndRemoved() {
101+
val reactor = Reactor<Int>()
102+
val input = reactor.InputCell(11)
103+
val output = reactor.ComputeCell(input) { it[0] + 1 }
104+
105+
val vals1 = mutableListOf<Int>()
106+
val sub1 = output.addCallback { vals1.add(it) }
107+
val vals2 = mutableListOf<Int>()
108+
output.addCallback { vals2.add(it) }
109+
110+
input.value = 31
111+
sub1.cancel()
112+
113+
val vals3 = mutableListOf<Int>()
114+
output.addCallback { vals3.add(it) }
115+
116+
input.value = 41
117+
118+
assertEquals(listOf(32), vals1)
119+
assertEquals(listOf(32, 42), vals2)
120+
assertEquals(listOf(42), vals3)
121+
}
122+
123+
@Ignore
124+
@Test
125+
fun removingACallbackMultipleTimesDoesntInterfereWithOtherCallbacks() {
126+
val reactor = Reactor<Int>()
127+
val input = reactor.InputCell(1)
128+
val output = reactor.ComputeCell(input) { it[0] + 1 }
129+
130+
val vals1 = mutableListOf<Int>()
131+
val sub1 = output.addCallback { vals1.add(it) }
132+
val vals2 = mutableListOf<Int>()
133+
output.addCallback { vals2.add(it) }
134+
135+
for (i in 1..10) {
136+
sub1.cancel()
137+
}
138+
139+
input.value = 2
140+
assertEquals(listOf<Int>(), vals1)
141+
assertEquals(listOf(3), vals2)
142+
}
143+
144+
@Ignore
145+
@Test
146+
fun callbacksShouldOnlyBeCalledOnceEvenIfMultipleDependenciesChange() {
147+
val reactor = Reactor<Int>()
148+
val input = reactor.InputCell(1)
149+
val plusOne = reactor.ComputeCell(input) { it[0] + 1 }
150+
val minusOne1 = reactor.ComputeCell(input) { it[0] - 1 }
151+
val minusOne2 = reactor.ComputeCell(minusOne1) { it[0] - 1 }
152+
val output = reactor.ComputeCell(plusOne, minusOne2) { it[0] * it[1] }
153+
154+
val vals = mutableListOf<Int>()
155+
output.addCallback { vals.add(it) }
156+
157+
input.value = 4
158+
assertEquals(listOf(10), vals)
159+
}
160+
161+
@Ignore
162+
@Test
163+
fun callbacksShouldNotBeCalledIfDependenciesChangeButOutputValueDoesntChange() {
164+
val reactor = Reactor<Int>()
165+
val input = reactor.InputCell(1)
166+
val plusOne = reactor.ComputeCell(input) { it[0] + 1 }
167+
val minusOne = reactor.ComputeCell(input) { it[0] - 1 }
168+
val alwaysTwo = reactor.ComputeCell(plusOne, minusOne) { it[0] - it[1] }
169+
170+
val vals = mutableListOf<Int>()
171+
alwaysTwo.addCallback { vals.add(it) }
172+
173+
for (i in 1..10) {
174+
input.value = i
175+
}
176+
177+
assertEquals(listOf<Int>(), vals)
178+
}
179+
}
180+
181+
@RunWith(Parameterized::class)
182+
// This is a digital logic circuit called an adder:
183+
// https://en.wikipedia.org/wiki/Adder_(electronics)
184+
class ReactAdderTest(val input: Input, val expected: Expected) {
185+
186+
companion object {
187+
data class Input(val a: Boolean, val b: Boolean, val carryIn: Boolean)
188+
data class Expected(val carryOut: Boolean, val sum: Boolean)
189+
190+
@JvmStatic
191+
@Parameterized.Parameters(name = "{index}: {0} = {1}")
192+
fun data() = listOf(
193+
arrayOf(Input(a=false, b=false, carryIn=false), Expected(carryOut=false, sum=false)),
194+
arrayOf(Input(a=false, b=false, carryIn=true), Expected(carryOut=false, sum=true)),
195+
arrayOf(Input(a=false, b=true, carryIn=false), Expected(carryOut=false, sum=true)),
196+
arrayOf(Input(a=false, b=true, carryIn=true), Expected(carryOut=true, sum=false)),
197+
arrayOf(Input(a=true, b=false, carryIn=false), Expected(carryOut=false, sum=true)),
198+
arrayOf(Input(a=true, b=false, carryIn=true), Expected(carryOut=true, sum=false)),
199+
arrayOf(Input(a=true, b=true, carryIn=false), Expected(carryOut=true, sum=false)),
200+
arrayOf(Input(a=true, b=true, carryIn=true), Expected(carryOut=true, sum=true))
201+
)
202+
}
203+
204+
@Ignore
205+
@Test
206+
fun test() {
207+
val reactor = Reactor<Boolean>()
208+
val a = reactor.InputCell(input.a)
209+
val b = reactor.InputCell(input.b)
210+
val carryIn = reactor.InputCell(input.carryIn)
211+
212+
val aXorB = reactor.ComputeCell(a, b) { it[0].xor(it[1]) }
213+
val sum = reactor.ComputeCell(aXorB, carryIn) { it[0].xor(it[1]) }
214+
215+
val aXorBAndCin = reactor.ComputeCell(aXorB, carryIn) { it[0] && it[1] }
216+
val aAndB = reactor.ComputeCell(a, b) { it[0] && it[1] }
217+
val carryOut = reactor.ComputeCell(aXorBAndCin, aAndB) { it[0] || it[1] }
218+
219+
assertEquals(expected, Expected(sum=sum.value, carryOut=carryOut.value))
220+
}
221+
}

exercises/settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ include 'pascals-triangle'
2424
include 'phone-number'
2525
include 'pig-latin'
2626
include 'raindrops'
27+
include 'react'
2728
include 'rna-transcription'
2829
include 'robot-name'
2930
include 'roman-numerals'

0 commit comments

Comments
 (0)