-
-
Notifications
You must be signed in to change notification settings - Fork 201
react: add to track #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
class Reactor<T>() { | ||
abstract inner class Cell { | ||
abstract val value: T | ||
internal val dependents = mutableListOf<ComputeCell>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interestingly, protected
is not possible, because then the c.dependents.add(this)
in the constructors all fail compilation with:
:compileKotlin
e: /home/pt/xkotlin/exercises/react/src/main/kotlin/React.kt: (22, 15): Cannot access 'dependents': it is protected in 'Cell'
e: /home/pt/xkotlin/exercises/react/src/main/kotlin/React.kt: (25, 16): Cannot access 'dependents': it is protected in 'Cell'
e: /home/pt/xkotlin/exercises/react/src/main/kotlin/React.kt: (26, 16): Cannot access 'dependents': it is protected in 'Cell'
FAILED
This is a little strange to me, since ComputeCell
is (I hope?!) a subclass of Cell
. Perhaps it doesn't apply inside constructors, or something like that... after all, you could consider the constructor to be a function not associated with any particular class, but it does return an instance of a particular class...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I'm likewise surprised that this error shows up for a protected
variable when being accessed in a subclass' constructor, it is available in the subclass outside of the constructor though...
All tests from x-common have been implemented. |
It is expected that by the time the student solution gets to this point, they will already have all they need to pass this test. The only point of this test is to show off the generics, and hopefully show a cool little gadget that can be made from a reactor.
c2.dependents.add(this) | ||
} | ||
|
||
fun addCallback(f: (T) -> Any): Subscription { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sigh, I changed too many intervening lines, so the old comment is on an "outdated diff" now. Gotta post it again.
I hope this is not too strange of an API. The API looks something like:
val cell: ComputeCell = ...
val sub = cell.addCallback { print(it) }
// ...
sub.cancel()
The alternative is to have addCallback
return some sort of ID:
val cell: ComputeCell = ...
val subID = cell.addCallback { print(it) }
// ...
cell.removeCallback(subID)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like what you did here returning back a Subscription
. Do you think we should provide some initial getting started help by at least providing some interfaces?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we should provide some initial getting started help by at least providing some interfaces?
I would be in support of that, because it may be otherwise difficult to determine from just the tests that addCallback is supposed to return something that you can call cancel()
on. It's easy enough to read function signatures if the expected return value is just a primitive, but if it needs to conform to an interface, then help is good.
By the way, it's time for me to point out the master list of all tracks' discussions on providing stubs exercism/discussions#114
I'll let y'all decide what y'all want to do with the other exercises, but for this one, how far should we go?
- only provide the Subscription interface, and specify that
addCallback
should return something that conforms to it? - provide Subscription, and stubs for
Cell
,ComputeCell
,InputCell
? But be careful, because this could constrain the student's implementation; maybe they don't want to have those exact three classes (maybe they just want to have a singleCell
class?
I'll start with the first option and see how it goes.
} | ||
|
||
inner class InputCell(initialValue: T) : Cell() { | ||
override var value: T = initialValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like http://kotlinlang.org/docs/reference/delegated-properties.html#observable could be used here, I'll consider it, though it's understandable enough without it.
(I'll probably not use it in the PR. Maybe in my submission to exercism.io!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, observables do seem like a great fit for this exercise - I wonder if there is a way to leverage the ObservableProperty as a super type. But what you currently have works, we should care more about if API driven by the tests make sense.
If #30 is merged before this PR, please notify me of such, so that I will know to add If this PR is merged before #30, please notify #30 of such, so that Unfortunately it does not make much sense for me to add |
@exercism/kotlin anyone available to give this PR a review? |
My apologies for leaving this hanging, I have been a bit preoccupied lately. I took a glance at the PR and thought it was overly complicated but that's because I didn't have an understanding of this particular exercise. I wanted to take a look at a couple of other tracks and was thinking of trying it out myself. Unfortunately I haven't had the time to do that yet - @jtigger do you have experience with this exercise and does it look reasonable to you? I would like to merge in the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution. Let me know what you think about the various comments, if you can also add the @Ignore
to all but the first test that would be appreciated.
config.json
Outdated
@@ -222,6 +222,11 @@ | |||
"difficulty": 1, | |||
"slug": "change", | |||
"topics": [] | |||
}, | |||
{ | |||
"difficulty": 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The difficulty should probably be either 9 or 10 I would think relative to the other exercises.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
difficulty: other tracks rated it:
- csharp, fsharp: 9
- ceylon: 10 (but note that was a unilateral decision by me, so doesn't provide the perspective of others)
- crystal, go, nim: not rated at this time (1)
- rust: 10 (but Rust only has four difficulty ratings, 1, 4, 7, and 10)
I'll rate 10 for this track since I used generics, which C# and F# didn't
class Reactor<T>() { | ||
abstract inner class Cell { | ||
abstract val value: T | ||
internal val dependents = mutableListOf<ComputeCell>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I'm likewise surprised that this error shows up for a protected
variable when being accessed in a subclass' constructor, it is available in the subclass outside of the constructor though...
abstract inner class Cell { | ||
abstract val value: T | ||
internal val dependents = mutableListOf<ComputeCell>() | ||
internal fun eachDependent(f: (ComputeCell) -> Any) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same thing as dependents.forEach { .... }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
forEach
embarrassing that I missed that!
} | ||
|
||
inner class InputCell(initialValue: T) : Cell() { | ||
override var value: T = initialValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, observables do seem like a great fit for this exercise - I wonder if there is a way to leverage the ObservableProperty as a super type. But what you currently have works, we should care more about if API driven by the tests make sense.
override var value: T = initialValue | ||
set(newValue) { | ||
field = newValue | ||
eachDependent { it.propagate() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As noted earlier you can just do: dependents.forEach { it.propagate() }
constructor(c: Cell, f: (T) -> T) : this({ f(c.value) }) { | ||
c.dependents.add(this) | ||
} | ||
constructor(c1: Cell, c2: Cell, f: (T, T) -> T) : this({ f(c1.value, c2.value) }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This constructor pattern is starting to scream for varargs, unfortunately I don't believe you can specify a vararg for lambda parameters. Is there a better way to perhaps go about these constructors for increased flexibility? Would passing a collection make more sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unfortunately I don't believe you can specify a vararg for lambda parameters
Yeah I didn't find one either, nor a way to make sure that the 1-cell form is always with a 1-argument lambda, a 2-cell form with a 2-argument lambda.
Is there a better way to perhaps go about these constructors for increased flexibility? Would passing a collection make more sense?
I believe this is possible. The disadvantage is that we lose a bit of compiler verification: Now someone can pass in two cells with a function that expects to get one value (it only reads the first value out of its collection of arguments). The advantage of course is more flexibility - we can imagine a compute cell that takes 3 cells, etc. even though it is not tested.
What I'm going to do is, if possible, I will make a separate commit that explores using a collection. We can decide whether to have that commit or not have it, when we see what it looks like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I have prepared what that looks like in the last commit, and we can decide whether it is better or worse to have it. Only disadvantage is as I mentioned the possibility of a caller mismatching the arities, giving one cell but expecting two values in the lambda, vice versa. Of course, since the test suite is in our control we know we haven't made that mistake, so the concern is only "what would happen if we used this as a library and gave it to other people to use, what kinds of mistakes might they make with it"
I personally like the varargs way slightly better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the way the lambda expression looks the old way but I do agree the vararg route seems better from an implementation perspective. In Kotlin 1.1 they are introducing destructing in lambdas which allows something like this: listOf(1, 2, 3).map { a, b, c -> "$a $b $c" }
. With that new feature we can re-introduce the better lambda readability without needing to change the method signature.
c2.dependents.add(this) | ||
} | ||
|
||
fun addCallback(f: (T) -> Any): Subscription { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like what you did here returning back a Subscription
. Do you think we should provide some initial getting started help by at least providing some interfaces?
companion object { | ||
@JvmStatic | ||
@Parameterized.Parameters(name = "{index}: {0} + {1} + {2} = {3} {4}") | ||
fun data() = listOf( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these parameters make it a bit difficult to read, perhaps something like this would be an improvement:
diff --git a/exercises/react/src/test/kotlin/ReactTest.kt b/exercises/react/src/test/kotlin/ReactTest.kt
index 92cc9e0..a514b6b 100644
--- a/exercises/react/src/test/kotlin/ReactTest.kt
+++ b/exercises/react/src/test/kotlin/ReactTest.kt
@@ -169,28 +169,32 @@ class ReactTest {
@RunWith(Parameterized::class)
// This is a digital logic circuit called an adder:
// https://en.wikipedia.org/wiki/Adder_(electronics)
-class ReactAdderTest(val aval: Boolean, val bval: Boolean, val cin: Boolean, val expectedCout: Boolean, val expectedSum: Boolean) {
+class ReactAdderTest(val input: Input, val expected: Expected) {
+
companion object {
+ data class Input(val a: Boolean, val b: Boolean, val carryIn: Boolean)
+ data class Expected(val carryOut: Boolean, val sum: Boolean)
+
@JvmStatic
- @Parameterized.Parameters(name = "{index}: {0} + {1} + {2} = {3} {4}")
+ @Parameterized.Parameters(name = "{index}: {0} = {1}")
fun data() = listOf(
- arrayOf(false, false, false, false, false),
- arrayOf(false, false, true, false, true),
- arrayOf(false, true, false, false, true),
- arrayOf(false, true, true, true, false),
- arrayOf(true, false, false, false, true),
- arrayOf(true, false, true, true, false),
- arrayOf(true, true, false, true, false),
- arrayOf(true, true, true, true, true)
+ arrayOf(Input(a=false, b=false, carryIn=false), Expected(carryOut=false, sum=false)),
+ arrayOf(Input(a=false, b=false, carryIn=true), Expected(carryOut=false, sum=true)),
+ arrayOf(Input(a=false, b=true, carryIn=false), Expected(carryOut=false, sum=true)),
+ arrayOf(Input(a=false, b=true, carryIn=true), Expected(carryOut=true, sum=false)),
+ arrayOf(Input(a=true, b=false, carryIn=false), Expected(carryOut=false, sum=true)),
+ arrayOf(Input(a=true, b=false, carryIn=true), Expected(carryOut=true, sum=false)),
+ arrayOf(Input(a=true, b=true, carryIn=false), Expected(carryOut=true, sum=false)),
+ arrayOf(Input(a=true, b=true, carryIn=true), Expected(carryOut=true, sum=true))
)
}
@Test
fun test() {
val reactor = Reactor<Boolean>()
- val a = reactor.InputCell(aval)
- val b = reactor.InputCell(bval)
- val carryIn = reactor.InputCell(cin)
+ val a = reactor.InputCell(input.a)
+ val b = reactor.InputCell(input.b)
+ val carryIn = reactor.InputCell(input.carryIn)
val aXorB = reactor.ComputeCell(a, b) { a, b -> a.xor(b) }
val sum = reactor.ComputeCell(aXorB, carryIn) { axorb, cin -> axorb.xor(cin) }
@@ -199,10 +203,6 @@ class ReactAdderTest(val aval: Boolean, val bval: Boolean, val cin: Boolean, val
val aAndB = reactor.ComputeCell(a, b) { a, b -> a && b }
val carryOut = reactor.ComputeCell(aXorBAndCin, aAndB) { a, b -> a || b }
- // Test them both at once so if they fail we get to see both values.
- assertEquals(
- listOf(expectedSum, expectedCout),
- listOf(sum.value, carryOut.value)
- )
+ assertEquals(expected, Expected(sum=sum.value, carryOut=carryOut.value))
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these parameters make it a bit difficult to read, perhaps something like this would be an improvement
So, I hope you don't mind if I just take that exactly as-is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review!
I'm adding changes as separate commits so reviewers can easily see what changed. It will probably be best to squash them all when merging. Or I can squash if that's what y'all want.
Still coming up are two items:
provide interface helpexplore whether we can use a collection for the ComputeCell ctor
Both done.
config.json
Outdated
@@ -222,6 +222,11 @@ | |||
"difficulty": 1, | |||
"slug": "change", | |||
"topics": [] | |||
}, | |||
{ | |||
"difficulty": 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
difficulty: other tracks rated it:
- csharp, fsharp: 9
- ceylon: 10 (but note that was a unilateral decision by me, so doesn't provide the perspective of others)
- crystal, go, nim: not rated at this time (1)
- rust: 10 (but Rust only has four difficulty ratings, 1, 4, 7, and 10)
I'll rate 10 for this track since I used generics, which C# and F# didn't
abstract inner class Cell { | ||
abstract val value: T | ||
internal val dependents = mutableListOf<ComputeCell>() | ||
internal fun eachDependent(f: (ComputeCell) -> Any) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
forEach
embarrassing that I missed that!
constructor(c: Cell, f: (T) -> T) : this({ f(c.value) }) { | ||
c.dependents.add(this) | ||
} | ||
constructor(c1: Cell, c2: Cell, f: (T, T) -> T) : this({ f(c1.value, c2.value) }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unfortunately I don't believe you can specify a vararg for lambda parameters
Yeah I didn't find one either, nor a way to make sure that the 1-cell form is always with a 1-argument lambda, a 2-cell form with a 2-argument lambda.
Is there a better way to perhaps go about these constructors for increased flexibility? Would passing a collection make more sense?
I believe this is possible. The disadvantage is that we lose a bit of compiler verification: Now someone can pass in two cells with a function that expects to get one value (it only reads the first value out of its collection of arguments). The advantage of course is more flexibility - we can imagine a compute cell that takes 3 cells, etc. even though it is not tested.
What I'm going to do is, if possible, I will make a separate commit that explores using a collection. We can decide whether to have that commit or not have it, when we see what it looks like.
c2.dependents.add(this) | ||
} | ||
|
||
fun addCallback(f: (T) -> Any): Subscription { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we should provide some initial getting started help by at least providing some interfaces?
I would be in support of that, because it may be otherwise difficult to determine from just the tests that addCallback is supposed to return something that you can call cancel()
on. It's easy enough to read function signatures if the expected return value is just a primitive, but if it needs to conform to an interface, then help is good.
By the way, it's time for me to point out the master list of all tracks' discussions on providing stubs exercism/discussions#114
I'll let y'all decide what y'all want to do with the other exercises, but for this one, how far should we go?
- only provide the Subscription interface, and specify that
addCallback
should return something that conforms to it? - provide Subscription, and stubs for
Cell
,ComputeCell
,InputCell
? But be careful, because this could constrain the student's implementation; maybe they don't want to have those exact three classes (maybe they just want to have a singleCell
class?
I'll start with the first option and see how it goes.
companion object { | ||
@JvmStatic | ||
@Parameterized.Parameters(name = "{index}: {0} + {1} + {2} = {3} {4}") | ||
fun data() = listOf( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these parameters make it a bit difficult to read, perhaps something like this would be an improvement
So, I hope you don't mind if I just take that exactly as-is.
We show the student about addCallback returning a Subscription. .keep file is removed, since there is now a file in the directory.
Added in #30 to all exercises so I need to add it too.
private var callbacksIssued = 0 | ||
private val activeCallbacks = mutableMapOf<Int, (T) -> Any>() | ||
|
||
constructor(vararg cells: Cell, f: (List<T>) -> T) : this({ f(cells.map { it.value }) }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
List
seemed most appropriate since it allowed indexing into it, so the lambdas can say it[0]
, it[0] + it[1]
, etc. Using Iterable
could work, but would be weird - we can do it.first
and it.fold { a, b -> a + b}
or something, I suppose.
config.json
Outdated
{ | ||
"difficulty": 10, | ||
"slug": "react", | ||
"topics": [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this track want to add any topics? I suggest a similar list as those in Ceylon https://github.com/exercism/xceylon/blob/master/config.json#L52
Reactive programming, generics, nested classes, higher-order functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, would be great to fill them in where we can.
This looks good to me, the only thing that is a little concerning is not having a more stubs available to point people in the correct syntax direction for some of the more complicated method signatures (such as the ComputeCell constructor). But we may be fine seeing how we ranked this the highest difficulty, does anyone know if there is any feedback mechanisms that may tip us if certain exercises are being more problematic than others? |
This will be interesting, because it would be possible for a student to go one of two ways:
So it could give insight to see how students take advantage of the freedom given to them, in this case. The other big potential stumbling block is the syntax for passing in a function as an argument. I actually imagine this can be alleviated based on the fact that this track has both accumulate and strain, which will require this.
Oh, often I wish for this too. exercism/discussions#86 mentions it . I don't think there is something implemented, and it may fold into the big user experience rethinking. |
Good points, I'm convinced :) Thanks for all the effort you put in this, it's a good challenge to throw at people. |
Thanks! |
Tests are taken from https://github.com/exercism/x-common/blob/master/exercises/react/canonical-data.json
There is one extra test to exercise the generics in Kotlin - this will be from the Rust track: https://github.com/exercism/xrust/blob/master/exercises/react/tests/react.rs#L223
Full disclosure: I ported this exercise to a few languages in the past:
The main reason I do this is it's a cheap way for me to get a taste of the language's capabilities, since it touches on a few interesting aspects. In Kotlin: generics, inheritance, visibility, abstract classes, properties and setters, inner classes, anonymous objects that satisfy an interface, higher order functions (declaration and use), constructors (primary and secondary), parameterized tests. So I satisfy my language curiosity, and hopefully, y'all get a usable exercise out of it.