-
Notifications
You must be signed in to change notification settings - Fork 8
Unexpected behavior when calling hooks a different # of times on a conditional branch #47
Comments
Adding here the great implementation ideas that @cristianoc proposed: pass a https://gist.github.com/cristianoc/0cb8afcfca272f17bb001ffa26983126 |
@cristianoc One of the first questions that I have about your proposal is that the initial value of For example, if a component does (very simplified): let comp = slots =>
useEffect(
slots,
() => print_endline("hello"),
(nextSlots, ()) =>
useState(
nextSlots,
3,
(_nextSlots, (count, setCount)) => {
setCount(count + 1);
();
},
),
); (i.e. use Then the let compSlot =
State.createSlot(slot2 => {
let slot1 = Effect.createSlot();
(slot1, (slot2, ()));
}); One idea I had was that we could include another creation function reason-reactify/lib/Reactify.re Line 165 in 1f4801a
But this would impose some extra burden on users, as they'd need to replicate the hooks function calls nesting structure into this Is there a way to solve this "slot creation" problem automatically through some type system mechanism? |
@jchavarri the issue with creating the initial value of An analogueForgetting about types for a second. If we were in an untyped world, and the data structure were an array, the natural solution would be to create an extensible array, which gets extended while we walk through it (without losing the identity, so next time the same array can be passed back to the functions). And we would pass the empty extensible array at the beginning. Extensible slotsNow back to the current situation: the data structure is nested pairs Initial valuesThere's the question of how to initialize slots, and the simplest way is to ask slot handlers to provide default values. Alternatively, one would make all slots be of option type, and initialize with None. Using defaults seems more general, as each slot handler can decide what to do, and using None is one specific decision. Here's a gist, which covers the simplest possible runnable examples: slots of type int and string: |
Woah @cristianoc this is awesome!! I think there is now a clear path to add a I will take a stab at implementing this solution in |
This sounds like a great approach! Thanks @cristianoc for the proposal and gist, and @jchavarri for implementing it 😄 Looking forward to it. I think this actually will solve a variety of 'technical debt' we have around our current-hacky-__global-state-solution. Wasn't happy with the way we shim state in and out of |
@cristianoc @bryphe I've started implementing something in the After the latest changes towards the "extensible slots model", what'd be the purpose of the continuation-based approach? If we have to thread the The accumulated tuple types generated by hooks usages would then live in the Do you see any downsides of removing the continuation? |
@jchavarri indeed one can go direct style, and do something like: let (x, slots) = slots |> useInt;
let (s, slots) = slots |> useString; Still need to return Here's an updated gist: https://gist.github.com/cristianoc/88d28074c86c276c5e501b1cb61b0ce2. |
Ok so I've been banging on this today, and I've got something working! |
The main problem that I'm finding with the new slots-based model is that the value of This is failing when trying to unify types when calling Here's a gist with an isolated case for it: I was thinking maybe about having a |
@jchavarri not sure about the general context, but for that example, this takes care of the type error: /* SlotsType: type empty = t(unit,unit); */
let (state, _nextSlots : Slots.empty) = useState(3, slots); Or, |
Aaah figured it out, it was exactly the second case you mentioned @cristianoc, I just had to ignore that type on the function signature, this example with direct style compiles! https://gist.github.com/jchavarri/ca0ec4f791c399fa3fba6bf8587b8755 🚀 @jaredly I am curious why the case without continuation doesn't work for |
@jaredly: I was wondering about this
That restriction is not actually needed when hooks are handled in a type-safe way.
instead of retrieving the state outside the conditional and do nothing with it. |
I don't think putting hooks in conditionals makes logical sense, even
though we can make sure it works type-wise
…On Sun, 6 Jan 2019, 6:16 am Cristiano Calcagno ***@***.*** wrote:
@jaredly <https://github.com/jaredly>: I was wondering about this
to enforce things like "don't put hooks inside of conditionals"
That restriction is not actually needed when hooks are handled in a
type-safe way.
Specifically, I'm thinking about:
if(showCounter) { renderCounter(hooks) }
instead of retrieving the state outside the conditional and do nothing
with it.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#47 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAG2Ks2coNXRhyghnYZC3F88s5iB9vZsks5vAfc_gaJpZM4ZnGC_>
.
|
As for obj.magic - reason reactify needs it in "pushNewState" to handle
hooks data. My impl doesn't use it anywhere.
…On Sun, 6 Jan 2019, 7:00 am Jared Forsyth ***@***.*** wrote:
I don't think putting hooks in conditionals makes logical sense, even
though we can make sure it works type-wise
On Sun, 6 Jan 2019, 6:16 am Cristiano Calcagno ***@***.***
wrote:
> @jaredly <https://github.com/jaredly>: I was wondering about this
>
> to enforce things like "don't put hooks inside of conditionals"
>
> That restriction is not actually needed when hooks are handled in a
> type-safe way.
> Specifically, I'm thinking about:
>
> if(showCounter) { renderCounter(hooks) }
>
> instead of retrieving the state outside the conditional and do nothing
> with it.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <#47 (comment)>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAG2Ks2coNXRhyghnYZC3F88s5iB9vZsks5vAfc_gaJpZM4ZnGC_>
> .
>
|
I am aware. This is the whole reason why we're exploring the slots approach 😄 I was asking because of this comment:
But if you had everything figured out, great! |
@cristianoc Thinking about it more, one potential advantage of the continuation-passing style is that we could return the latest state value from the I haven't still figured out all the details, but here's the current state of this idea: https://gist.github.com/jchavarri/e90f1f49df51922989b200f81d925202 Although, this might be possible as well with the direct style... |
@jchavarri Not sure I understand the comment, but slots gets filled the first time foo(slots) is called. And the next time foo(slots) is called, all the slots are already filled so foo gets access to them. |
I'm running into an issue with the slots polymorphism. When reconciling, we need a way to set the "initial slot" to But we don't want the instance to know the full type of the slots as that would complicate things. So I'm wrapping the let render = (id: ComponentId.t, lazyElement, ~children) => {
ignore(children);
let ret =
Component(
id,
(OpaqueSlot(slots)) => {
Effects.resetEffects(__globalEffects);
let childElement = lazyElement(slots); /* This fails: "The type constructor $OpaqueSlot_'slot would escape its scope" */
let children = [childElement];
let effects = Effects.getEffects(__globalEffects);
let renderResult: elementWithChildren = (
children,
effects,
__globalContext^,
);
renderResult;
},
);
ret;
}; The alternative (not using GADT and storing the slots types "as is" in the instance) is prob not an option as that polymorphism would propagate across the instances which is problematic for children etc. Is there a way to store the slots without exposing the polymorphism, and pass them down unpacked to the user defined function? I see @jaredly managed to do it by creating a record with an |
Hm, I was trying to create an isolated case and ran into a different issue 😅 When trying to add the slots the function used in https://gist.github.com/jchavarri/0455be2984480313bd1800b14b01251c |
@jchavarri ah now I better understand your question. With the non-cps implementation, there's no way to "unwind" the slots after they've been created, so I wasn't able to get the "final" state of the slots to match the "initial" state of the slots that was expected. CPS made it so that the final state was the same type as the initial state |
@jchavarri as far as polymorphism, I pack the slots along with the render function, so that the render function is allowed to know about the slots type https://github.com/jaredly/fluid/blob/master/src/Fluid.re#L86 |
Thanks a lot @jaredly ! Your suggestions and the Fluid code (amazing progress btw! 😮 ), plus some hints from @cristianoc –who helped me offline– brought me back to the right path. The trick as both of you mentioned was to pack the slots with the render function. (aside: I wish this kind of "advanced OCaml folk knowledge" could be learnt upfront by reading some manual, book or docs that explain how these types work, but I guess this is the most efficient way to learn? 🤔 ) I have an isolated example with a similar setup to the one in Now it is a matter of integrating these ideas into the existing
@jaredly I'm not sure I understand this. With the slots implementation that @cristianoc shared in https://gist.github.com/cristianoc/88d28074c86c276c5e501b1cb61b0ce2, one doesn't need to unwind the slots, the type of the |
Oh you're totally right. I was missing having each slot be a ref. Looks great! |
While #43 made some progress on solidifying hooks types to make their usage safer, there is still a case that @cristianoc brought up today that is not covered. For example, if one replaces the
renderCounter
function by:(note the additional
useState
call on the second branch wrapped byignore()
)This code will compile, because in both branches we return
hook((unit, reducer((int, action) => int)))
. But at runtime, the behavior becomes unexpected, because of the differences "state stacks" that are created between branches. In some cases (like if the ignoreduseState
using a string-typed state) leading to runtime exceptions because of the different types ofstate[0]
for that component.@cristianoc also came up with this (amazing) idea that now that we know the shape of the state of each component in the form of nested tuples, we could change the underlying implementation to support this kind of behavior by moving away from the state stack model, into a new model where each hook has a reserved "slot" with the shape of the state needed.
If I understood correctly, the change would be that the hooks would return a "getter" and a "setter" for that individual piece of state, which means there would be no linear restriction about hooks anymore, as one would always get the "right paths" to read and write from them.
I believe that the implementation of these ideas could also lead to the removal of the
Obj.magic
that is currently used inState.re
, but I have to noodle a bit more around this 🍜.@bryphe Would you be open to more API changes to enforce a better safety? (even at the cost of diverging a bit from ReactJS hooks semantics...)
The text was updated successfully, but these errors were encountered: