From 68eb1cb2b09d12fc160cc758031e96f5b3c59d74 Mon Sep 17 00:00:00 2001 From: Jeremiah Zucker Date: Thu, 16 May 2024 17:14:12 -0400 Subject: [PATCH 1/3] state hook first attempt --- .../com/intuit/hooks/ImperativeTests.kt | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt diff --git a/hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt b/hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt new file mode 100644 index 0000000..7aaf34b --- /dev/null +++ b/hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt @@ -0,0 +1,290 @@ +package com.intuit.hooks + +import com.intuit.hooks.SyncHookTests.Hook1 +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import java.lang.ref.WeakReference +import kotlin.reflect.KProperty + +// TODO: Given that we require some knowledge of the return value for tapping the +// hook, we can only apply this strategy for hooks that expect `Unit`. We +// could potentially have a special hook type for this, called `StateHook`. +// This'd remove the need for an intermediate capture class. We would want +// to have helpers for converting `SyncHook` -> `StateHook`. + +class Capture(hook: Hook1) { + private var ref: WeakReference? = null + + init { + hook.tap("capture") { _, incoming -> + ref = incoming?.let(::WeakReference) + } + } +} + +// this is _kinda_ a hook.. but it's really just a wrapper that can only be instantiated with a reference to another hook +class StateHook(hook: SyncHook<(HookContext, T) -> Unit>): SyncHook<(HookContext, T) -> Unit>() { + private var ref: WeakReference? = null + + init { + hook.tap("StateHook") { ctx, incoming -> + ref?.clear() + ref = incoming?.let(::WeakReference) + call(ctx, incoming) + } + } + + fun clear() { + ref?.clear() + } + + fun call(p1: T) = super.call { f, context -> f(context, p1) } + + fun call(context: HookContext, p1: T) = super.call { f, _ -> f(context, p1) } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): T? = ref?.get() +} + +fun SyncHook<(HookContext, T) -> Unit>.asStateHook() = StateHook(this) + +fun SyncHook<(HookContext, T) -> Unit>.map(block: (T) -> R): SyncHook<(HookContext, R) -> Unit> { + val transformed = Hook1().asStateHook() // TODO: necessary for propagating context + tap("map") { context, incoming -> transformed.call(context, block(incoming)) } + return transformed +} + +// the inherent problem here, is we need R to be nullable if T is nullable. We've tried a few approaches, but since nullability isn't captured +// as part of the JVM type system, we get platform declaration clashes when narrowing scenarios to different overloads +inline fun SyncHook<(HookContext, T?) -> Unit>.flatMapNullable(crossinline block: (T?) -> SyncHook<(HookContext, R) -> Unit>?): SyncHook<(HookContext, R?) -> Unit> { + // TODO: I hate that we need to return a R? state hook here - it means downstream consumers are no longer guaranteed to have a non-nullable + val transformed = Hook1().asStateHook() // TODO: necessary for propagating context + // TODO: Do we need to unregister the tap with a new incoming value? + tap("flatMap") { _, incoming -> block(incoming)?.tap("capture", transformed::call) ?: transformed.call(null) } + return transformed +} + +inline fun SyncHook<(HookContext, T) -> Unit>.flatMap(crossinline block: (T) -> SyncHook<(HookContext, R) -> Unit>?): SyncHook<(HookContext, R) -> Unit> { + // TODO: I hate that we need to return a R? state hook here - it means downstream consumers are no longer guaranteed to have a non-nullable + val transformed = Hook1().asStateHook() // TODO: necessary for propagating context + // TODO: Do we need to unregister the tap with a new incoming value? + tap("flatMap") { _, incoming -> block(incoming)?.tap("capture", transformed::call) ?: transformed.clear() } + return transformed +} + +// TODO: I wish this could have the same overload signature as [asStateHook] below, but the return type erasure isn't allowing for it +fun SyncHook<(HookContext, T) -> Unit>.mapAsStateHook(block: (T) -> R): StateHook = map(block).asStateHook() +// val captured = Hook1().asStateHook() +// tap("asStateHook") { _, t -> +// t?.let(block)?.let(captured::call) +// } +// return captured + +inline fun SyncHook<(HookContext, T) -> Unit>.flatMapAsStateHook(crossinline block: (T) -> SyncHook<(HookContext, R) -> Unit>?): StateHook = + flatMap(block).asStateHook() + +inline fun SyncHook<(HookContext, T?) -> Unit>.flatMapNullableAsStateHook(crossinline block: (T?) -> SyncHook<(HookContext, R) -> Unit>?): StateHook = + flatMapNullable(block).asStateHook() +// val captured = Hook1().asStateHook() +// tap("asStateHook") { _, t -> +// t?.let(block)?.tap("capture", captured::call) +// } +// return captured + +class ImperativeTests { + + @Test fun `as state hook`() { + val hook = Hook1() + + val stateHook = hook.asStateHook() + val state by stateHook + var stateHookValue: String? = null + stateHook.tap("happy path") { _, value -> + stateHookValue = value + } + + // edge case, but maybe we just avoid an API like this + val nestedStateHook = stateHook.asStateHook() + val nestedState by nestedStateHook + var nestedStateHookValue: String? = null + nestedStateHook.tap("happy path") { _, value -> + nestedStateHookValue = value + } + + assertNull(state) + hook.call("hello") + assertEquals("hello", state) + assertEquals("hello", stateHookValue) + assertEquals("hello", nestedState) + assertEquals("hello", nestedStateHookValue) + } + + data class Container(val name: String) { + val containerHook = Hook1() + val nullableContainerHook = Hook1() + + val containerState = containerHook.asStateHook() + val nullableContainerState = nullableContainerHook.asStateHook() + } + + @Test fun `nested state hook`() { + val outer = Container("outer") + val inner = Container("inner") + val nested = Container("nested") + + // statehooks won't capture values pushed before creation + outer.containerHook.call(inner) + outer.nullableContainerHook.call(inner) + + // 1 level deep + val innerState by outer.containerHook.asStateHook() + val innerNameState by outer.containerHook.mapAsStateHook(Container::name) + val nullableInnerState by outer.nullableContainerHook.asStateHook() + // TODO: Can we make the last part of this accept a lambda reference, essentially, preserve `null` for empty case and treat blocks as operating on non-nulls? + val nullableInnerNameState by outer.nullableContainerHook.mapAsStateHook { it?.name } + + assertNull(innerState) + assertNull(innerNameState) + assertNull(nullableInnerState) + assertNull(nullableInnerNameState) + + outer.containerHook.call(inner) + + assertEquals(inner, innerState) + assertEquals("inner", innerNameState) + assertNull(nullableInnerState) + assertNull(nullableInnerNameState) + + outer.nullableContainerHook.call(inner) + + assertEquals(inner, nullableInnerState) + assertEquals("inner", nullableInnerNameState) + + outer.nullableContainerHook.call(null) + + assertNull(nullableInnerState) + assertNull(nullableInnerNameState) + + // 2 levels deep, with non-nullable outer + val nestedInnerState by outer.containerHook.flatMapAsStateHook(Container::containerHook) + val nestedInnerNameState by outer.containerHook.flatMapAsStateHook(Container::containerHook).mapAsStateHook(Container::name) + val nestedNullableInnerState by outer.containerHook.flatMapAsStateHook(Container::nullableContainerHook) + val nestedNullableInnerNameState by outer.containerHook.flatMapAsStateHook(Container::nullableContainerHook).mapAsStateHook { it?.name } + + assertNull(nestedInnerState) + assertNull(nestedInnerNameState) + assertNull(nestedNullableInnerState) + assertNull(nestedNullableInnerNameState) + + outer.containerHook.call(inner) + + assertNull(nestedInnerState) + assertNull(nestedInnerNameState) + assertNull(nestedNullableInnerState) + assertNull(nestedNullableInnerNameState) + + inner.containerHook.call(nested) + + assertEquals(nested, nestedInnerState) + assertEquals("nested", nestedInnerNameState) + assertNull(nestedNullableInnerState) + assertNull(nestedNullableInnerNameState) + + inner.nullableContainerHook.call(nested) + + assertEquals(nested, nestedNullableInnerState) + assertEquals("nested", nestedNullableInnerNameState) + + inner.nullableContainerHook.call(null) + + assertNull(nestedNullableInnerState) + assertNull(nestedNullableInnerNameState) + + // 2 levels deep, with nullable outer + val nestedNullableOuterInnerState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.containerHook } + val nestedNullableOuterInnerNameState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.containerHook }.mapAsStateHook { it?.name } + val nestedNullableOuterNullableInnerState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.nullableContainerHook } + val nestedNullableOuterNullableInnerNameState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.nullableContainerHook }.mapAsStateHook { it?.name } + + assertNull(nestedNullableOuterInnerState) + assertNull(nestedNullableOuterInnerNameState) + assertNull(nestedNullableOuterNullableInnerState) + assertNull(nestedNullableOuterNullableInnerNameState) + + outer.nullableContainerHook.call(inner) + + assertNull(nestedNullableOuterInnerState) + assertNull(nestedNullableOuterInnerNameState) + assertNull(nestedNullableOuterNullableInnerState) + assertNull(nestedNullableOuterNullableInnerNameState) + + inner.containerHook.call(nested) + + assertEquals(nested, nestedNullableOuterInnerState) + assertEquals("nested", nestedNullableOuterInnerNameState) + assertNull(nestedNullableOuterNullableInnerState) + assertNull(nestedNullableOuterNullableInnerNameState) + + inner.nullableContainerHook.call(nested) + + assertEquals(nested, nestedNullableOuterNullableInnerState) + assertEquals("nested", nestedNullableOuterNullableInnerNameState) + + inner.nullableContainerHook.call(null) + + assertNull(nestedNullableOuterNullableInnerState) + assertNull(nestedNullableOuterNullableInnerNameState) + + // reset nullable to ensure we test clearing state from the top-level + inner.nullableContainerHook.call(nested) + outer.nullableContainerHook.call(null) + + assertNull(nestedNullableOuterInnerState) + assertNull(nestedNullableOuterInnerNameState) + assertNull(nestedNullableOuterNullableInnerState) + assertNull(nestedNullableOuterNullableInnerNameState) + } + + @Test fun `doubly nested nullable hooks`() { + val outer = Container("outer") + val inner = Container("inner") + val nested = Container("nested") + + // 2 levels deep, with nullable outer + val nestedNullableOuterNullableInnerState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.nullableContainerHook } + val nestedNullableOuterNullableInnerNameState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.nullableContainerHook }.mapAsStateHook { it?.name } + + outer.nullableContainerHook.call(inner) + inner.nullableContainerHook.call(nested) + + assertEquals(nested, nestedNullableOuterNullableInnerState) + assertEquals("nested", nestedNullableOuterNullableInnerNameState) + + outer.nullableContainerHook.call(null) + + assertNull(nestedNullableOuterNullableInnerState) + assertNull(nestedNullableOuterNullableInnerNameState) + } + + @Test fun `doubly nested outer non-nullable hooks`() { + val outer = Container("outer") + val inner = Container("inner") + val nested = Container("nested") + + // 2 levels deep, with nullable outer + val nestedOuterNullableInnerState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.containerHook } + val nestedOuterNullableInnerNameState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.containerHook }.mapAsStateHook { it?.name } + + outer.nullableContainerHook.call(inner) + inner.containerHook.call(nested) + + assertEquals(nested, nestedOuterNullableInnerState) + assertEquals("nested", nestedOuterNullableInnerNameState) + + outer.nullableContainerHook.call(null) + + assertNull(nestedOuterNullableInnerState) + assertNull(nestedOuterNullableInnerNameState) + } +} From 3a984a5b83acfd49e96927b27d2e0d3ff7ce0a85 Mon Sep 17 00:00:00 2001 From: Jeremiah Zucker Date: Tue, 21 May 2024 18:49:18 -0400 Subject: [PATCH 2/3] some cleanup and more functional methods --- .../com/intuit/hooks/ImperativeTests.kt | 196 ++++++++++++------ 1 file changed, 130 insertions(+), 66 deletions(-) diff --git a/hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt b/hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt index 7aaf34b..e67ec3c 100644 --- a/hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt +++ b/hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt @@ -1,99 +1,163 @@ package com.intuit.hooks import com.intuit.hooks.SyncHookTests.Hook1 -import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.lang.ref.WeakReference import kotlin.reflect.KProperty // TODO: Given that we require some knowledge of the return value for tapping the -// hook, we can only apply this strategy for hooks that expect `Unit`. We -// could potentially have a special hook type for this, called `StateHook`. -// This'd remove the need for an intermediate capture class. We would want -// to have helpers for converting `SyncHook` -> `StateHook`. - -class Capture(hook: Hook1) { - private var ref: WeakReference? = null - - init { - hook.tap("capture") { _, incoming -> - ref = incoming?.let(::WeakReference) - } - } -} +// hook, we can only apply this strategy for hooks that expect `Unit` (or +// the same return type). We could potentially have a special hook type for +// this, called `StateHook`. This'd remove the need for an intermediate capture +// class. We would want to have helpers for converting `SyncHook` -> `StateHook`. // this is _kinda_ a hook.. but it's really just a wrapper that can only be instantiated with a reference to another hook class StateHook(hook: SyncHook<(HookContext, T) -> Unit>): SyncHook<(HookContext, T) -> Unit>() { private var ref: WeakReference? = null + public val value: T? get() = ref?.get() + + public operator fun getValue(thisRef: Any?, property: KProperty<*>): T? = value + init { + // configure updates from incoming values from parent hook hook.tap("StateHook") { ctx, incoming -> - ref?.clear() - ref = incoming?.let(::WeakReference) + onValue(ctx, incoming) call(ctx, incoming) } + + // configure updates from mutations on this hook + tap("StateHook", ::onValue) + + // enable replay cache for new tappers + interceptRegister { tap -> + value?.let { tap.f(hashMapOf(), it) } + tap + } } - fun clear() { + private fun onValue(context: HookContext, incoming: T) { ref?.clear() + ref = incoming?.let(::WeakReference) } - fun call(p1: T) = super.call { f, context -> f(context, p1) } - - fun call(context: HookContext, p1: T) = super.call { f, _ -> f(context, p1) } + internal fun clear() { + ref?.clear() + } - operator fun getValue(thisRef: Any?, property: KProperty<*>): T? = ref?.get() + internal fun call(p1: T) = call { f, context -> f(context, p1) } + // special call to propagate incoming context from wrapped hook - maybe uplevel? + internal fun call(context: HookContext, p1: T) = call { f, _ -> f(context, p1) } } -fun SyncHook<(HookContext, T) -> Unit>.asStateHook() = StateHook(this) +fun SyncHook<(HookContext, T) -> Unit>.asStateHook(): StateHook = if (this is StateHook) this else StateHook(this) + +fun SyncHook<(HookContext, T) -> Unit>.filter(predicate: (T) -> Boolean): SyncHook<(HookContext, T) -> Unit> { + val filtered = Hook1().asStateHook() + tap("filter") { context, incoming -> if (predicate(incoming)) filtered.call(context, incoming) } + return filtered +} fun SyncHook<(HookContext, T) -> Unit>.map(block: (T) -> R): SyncHook<(HookContext, R) -> Unit> { - val transformed = Hook1().asStateHook() // TODO: necessary for propagating context + val transformed = Hook1().asStateHook() tap("map") { context, incoming -> transformed.call(context, block(incoming)) } return transformed } // the inherent problem here, is we need R to be nullable if T is nullable. We've tried a few approaches, but since nullability isn't captured // as part of the JVM type system, we get platform declaration clashes when narrowing scenarios to different overloads -inline fun SyncHook<(HookContext, T?) -> Unit>.flatMapNullable(crossinline block: (T?) -> SyncHook<(HookContext, R) -> Unit>?): SyncHook<(HookContext, R?) -> Unit> { - // TODO: I hate that we need to return a R? state hook here - it means downstream consumers are no longer guaranteed to have a non-nullable - val transformed = Hook1().asStateHook() // TODO: necessary for propagating context - // TODO: Do we need to unregister the tap with a new incoming value? - tap("flatMap") { _, incoming -> block(incoming)?.tap("capture", transformed::call) ?: transformed.call(null) } +// we _could_ potentially solve this by introducing a sealed type to encapsulate the return type of state hooks -- this'd enable us to +// ensure the statehook respects the parent hook type, while enabling us to capture "empty" state w/o using null +fun SyncHook<(HookContext, T?) -> Unit>.flatMapNullable(block: (T?) -> SyncHook<(HookContext, R) -> Unit>?): SyncHook<(HookContext, R?) -> Unit> { + val transformed = Hook1().asStateHook() + tap("flatMapNullable") { _, incoming -> block(incoming)?.tap("flatMapNullable", transformed::call) ?: transformed.call(null) } return transformed } -inline fun SyncHook<(HookContext, T) -> Unit>.flatMap(crossinline block: (T) -> SyncHook<(HookContext, R) -> Unit>?): SyncHook<(HookContext, R) -> Unit> { - // TODO: I hate that we need to return a R? state hook here - it means downstream consumers are no longer guaranteed to have a non-nullable - val transformed = Hook1().asStateHook() // TODO: necessary for propagating context - // TODO: Do we need to unregister the tap with a new incoming value? - tap("flatMap") { _, incoming -> block(incoming)?.tap("capture", transformed::call) ?: transformed.clear() } +fun SyncHook<(HookContext, T) -> Unit>.flatMap(block: (T) -> SyncHook<(HookContext, R) -> Unit>?): SyncHook<(HookContext, R) -> Unit> { + val transformed = Hook1().asStateHook() + tap("flatMap") { _, incoming -> block(incoming)?.tap("flatMap", transformed::call) ?: transformed.clear() } return transformed } -// TODO: I wish this could have the same overload signature as [asStateHook] below, but the return type erasure isn't allowing for it -fun SyncHook<(HookContext, T) -> Unit>.mapAsStateHook(block: (T) -> R): StateHook = map(block).asStateHook() -// val captured = Hook1().asStateHook() -// tap("asStateHook") { _, t -> -// t?.let(block)?.let(captured::call) -// } -// return captured - -inline fun SyncHook<(HookContext, T) -> Unit>.flatMapAsStateHook(crossinline block: (T) -> SyncHook<(HookContext, R) -> Unit>?): StateHook = - flatMap(block).asStateHook() - -inline fun SyncHook<(HookContext, T?) -> Unit>.flatMapNullableAsStateHook(crossinline block: (T?) -> SyncHook<(HookContext, R) -> Unit>?): StateHook = - flatMapNullable(block).asStateHook() -// val captured = Hook1().asStateHook() -// tap("asStateHook") { _, t -> -// t?.let(block)?.tap("capture", captured::call) -// } -// return captured +// potentially add linter rule for .map().flatten() to just use .flatMap() +fun Unit>, R> SyncHook<(HookContext, T) -> Unit>.flatten(): SyncHook<(HookContext, R) -> Unit> = + flatMap { it } + +fun Unit>, R> SyncHook<(HookContext, T?) -> Unit>.flattenNullable(): SyncHook<(HookContext, R?) -> Unit> = + flatMapNullable { it } class ImperativeTests { + @Test fun `simple state hook`() { + val nameHook = Hook1().asStateHook() + val name: String? by nameHook + + assertNull(name) + + nameHook.call("this is my name") + + assertEquals("this is my name", name) + + var replayed = false + nameHook.tap("test") { _, name -> + assertEquals("this is my name", name) + replayed = true + } + + assertTrue(replayed) + } + + class Machine { + val someControllerHook = Hook1() + + // setting up a state hook at this level would enable tapping + val someControllerState = someControllerHook.asStateHook() + } + + data class SomeController(val id: String) { + val nestedControllerHook = Hook1() + } + + @Test fun `map state hook`() { + val machine = Machine() + val controller = SomeController("id") + + val controllerId by machine.someControllerHook + .map(SomeController::id) + .asStateHook() + + machine.someControllerHook.call(controller) + + assertEquals("id", controllerId) + } + + @Test fun `flatmap state hook`() { + val machine = Machine() + val controller = SomeController("outer") + val nestedController = SomeController("nested") + + val nestedControllerId by machine.someControllerHook + .flatMap(SomeController::nestedControllerHook) + .map(SomeController::id) + .asStateHook() + +// var id: String? = null +// machine.someControllerHook.tap("") { _, someController -> +// someController.nestedControllerHook.tap("") { _, nestedController -> +// id = nestedController.id +// } +// } + + machine.someControllerHook.call(controller) + controller.nestedControllerHook.call(nestedController) + + assertEquals("nested", nestedControllerId) + } + @Test fun `as state hook`() { val hook = Hook1() @@ -139,10 +203,10 @@ class ImperativeTests { // 1 level deep val innerState by outer.containerHook.asStateHook() - val innerNameState by outer.containerHook.mapAsStateHook(Container::name) + val innerNameState by outer.containerHook.map(Container::name).asStateHook() val nullableInnerState by outer.nullableContainerHook.asStateHook() // TODO: Can we make the last part of this accept a lambda reference, essentially, preserve `null` for empty case and treat blocks as operating on non-nulls? - val nullableInnerNameState by outer.nullableContainerHook.mapAsStateHook { it?.name } + val nullableInnerNameState by outer.nullableContainerHook.map { it?.name }.asStateHook() assertNull(innerState) assertNull(innerNameState) @@ -167,10 +231,10 @@ class ImperativeTests { assertNull(nullableInnerNameState) // 2 levels deep, with non-nullable outer - val nestedInnerState by outer.containerHook.flatMapAsStateHook(Container::containerHook) - val nestedInnerNameState by outer.containerHook.flatMapAsStateHook(Container::containerHook).mapAsStateHook(Container::name) - val nestedNullableInnerState by outer.containerHook.flatMapAsStateHook(Container::nullableContainerHook) - val nestedNullableInnerNameState by outer.containerHook.flatMapAsStateHook(Container::nullableContainerHook).mapAsStateHook { it?.name } + val nestedInnerState by outer.containerHook.flatMap(Container::containerHook).asStateHook() + val nestedInnerNameState by outer.containerHook.flatMap(Container::containerHook).map(Container::name).asStateHook() + val nestedNullableInnerState by outer.containerHook.flatMap(Container::nullableContainerHook).asStateHook() + val nestedNullableInnerNameState by outer.containerHook.flatMap(Container::nullableContainerHook).map { it?.name }.asStateHook() assertNull(nestedInnerState) assertNull(nestedInnerNameState) @@ -202,10 +266,10 @@ class ImperativeTests { assertNull(nestedNullableInnerNameState) // 2 levels deep, with nullable outer - val nestedNullableOuterInnerState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.containerHook } - val nestedNullableOuterInnerNameState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.containerHook }.mapAsStateHook { it?.name } - val nestedNullableOuterNullableInnerState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.nullableContainerHook } - val nestedNullableOuterNullableInnerNameState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.nullableContainerHook }.mapAsStateHook { it?.name } + val nestedNullableOuterInnerState by outer.nullableContainerHook.flatMapNullable { it?.containerHook }.asStateHook() + val nestedNullableOuterInnerNameState by outer.nullableContainerHook.flatMapNullable { it?.containerHook }.map { it?.name }.asStateHook() + val nestedNullableOuterNullableInnerState by outer.nullableContainerHook.flatMapNullable { it?.nullableContainerHook }.asStateHook() + val nestedNullableOuterNullableInnerNameState by outer.nullableContainerHook.flatMapNullable { it?.nullableContainerHook }.map { it?.name }.asStateHook() assertNull(nestedNullableOuterInnerState) assertNull(nestedNullableOuterInnerNameState) @@ -252,8 +316,8 @@ class ImperativeTests { val nested = Container("nested") // 2 levels deep, with nullable outer - val nestedNullableOuterNullableInnerState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.nullableContainerHook } - val nestedNullableOuterNullableInnerNameState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.nullableContainerHook }.mapAsStateHook { it?.name } + val nestedNullableOuterNullableInnerState by outer.nullableContainerHook.flatMapNullable { it?.nullableContainerHook }.asStateHook() + val nestedNullableOuterNullableInnerNameState by outer.nullableContainerHook.flatMapNullable { it?.nullableContainerHook }.map { it?.name }.asStateHook() outer.nullableContainerHook.call(inner) inner.nullableContainerHook.call(nested) @@ -273,8 +337,8 @@ class ImperativeTests { val nested = Container("nested") // 2 levels deep, with nullable outer - val nestedOuterNullableInnerState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.containerHook } - val nestedOuterNullableInnerNameState by outer.nullableContainerHook.flatMapNullableAsStateHook { it?.containerHook }.mapAsStateHook { it?.name } + val nestedOuterNullableInnerState by outer.nullableContainerHook.flatMapNullable { it?.containerHook }.asStateHook() + val nestedOuterNullableInnerNameState by outer.nullableContainerHook.flatMapNullable { it?.containerHook }.map { it?.name }.asStateHook() outer.nullableContainerHook.call(inner) inner.containerHook.call(nested) From 5a05de6d6507bc0c43645a8841958ea46be9579e Mon Sep 17 00:00:00 2001 From: Jeremiah Zucker Date: Tue, 21 May 2024 19:21:46 -0400 Subject: [PATCH 3/3] finish comment --- hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt b/hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt index e67ec3c..3aeab36 100644 --- a/hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt +++ b/hooks/src/test/kotlin/com/intuit/hooks/ImperativeTests.kt @@ -114,7 +114,7 @@ class ImperativeTests { class Machine { val someControllerHook = Hook1() - // setting up a state hook at this level would enable tapping + // setting up a state hook at this level would enable tapping at any point to get the state value val someControllerState = someControllerHook.asStateHook() }