-
Notifications
You must be signed in to change notification settings - Fork 62
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
Allowing records and tuples to contain functions #390
Comments
Some previous discussions: From memory, I guess the TLDR from 292 would be:
Personally, I don't think 1 is generally useful and in the special cases where it is desired (eg, More recently, the treatment of equality of R/T values has also been reconsidered, meaning use cases such as your |
Thank you for the links, interesting discussions.
I reaaaalllyy hope they don't give up the equality part. Agree that that's what makes this proposal interesting. The function thing is not such a biggie, I could always just use a WeakMap and symbols, it just would be nice if I didn't have to. |
It’s not just about changing, it’s about being a communications channel. A frozen function can still repeatedly return a mutable object. |
Functions are also objects. So this would be possible: const aFunction = () => {};
aFunction.staticProp = 1;
const tuple = #[aFunction];
aFunction.staticProp = 2; This would not meet many people's definition of 'deeply immutable'. It is deeply immutable if the function is considered only as 'a reference to a function' - in that regard it isn't changing. I do agree that not allowing functions directly limits the use cases. Allowing functions opens up use cases, but introduces mutable objects. Different people weigh up those two sides differently.
There is existing code that assumes any value where the BTW if you are interested I tried to make a website that helps explain the interconnectedness of the R&T design space: https://acutmore.github.io/record-tuple-laboratory/ |
Thank you for the playground, that's hilarious. Will look at it more later.
Okay and could that be fixed by only allow adding frozen functions into R&T? This way one could just wrap the functions in And the code that assumes records and tuples to be inert would still be correct. You'd only have to add a check when creating records & tuples that functions are frozen. Arrrgggh ok I already realise that this wouldn't work because "deep-freezing" isn't easy and one could have another object already as property on the function that one then changes. And it's not easy to check if functions are "unmodified", meaning they don't contain any other properties than what's needed to execute them and then only allow such functions? |
Freezing, even deeply, wouldn't be sufficient. One there is no guarantee that a frozen function is not in fact a proxy, which records and tuples would guarantee. Two, as mentioned previously, the function would be free to return mutable objects, which would be against what some consider as deeply immutable |
There are also other differences between functions and records or tuples:
R/T as primitives (with their lack of prototype) means very good support for being sent across realms or serialized/structured cloned. |
There may be a possibility that could be acceptable to stakeholders:
This is sort of a variation of the "Box" idea, but without equality Edit: The reason this may be acceptable is that an object containing a function is likely already considered today as not being pure. |
Oh I never thought about this, is it not possible to proxy records and tuples at all? |
If they have typeof |
Ahh right. I just wonder how that would work with equality but I think it just doesn't.
Appreciate the idea but now my original usecase is broken. Thank you all for the answers, I'll just go use a WeakMap with symbols if I ever really need to have a function in a record or tuple. And I hope this proposal moves forward soon. Already today using it in form of the polyfill provides nice benefits IMO. |
The main reason why I am interested in R/T is to have "compound primitive" types which are better than As an example use, let's imagine that I want to represent a map from Having functions, or any boxed type, means that this property of being able to create similar records from scratch is no longer there. You'd have to keep reuse the same function reference, but if you already have to keep references around then I feel that it weakens the use-case: just keep a ref to the wrapper object instead. (There are still some cases where it's useful to only rebuild the wrapper object, but I feel like it's a less compelling use case). |
I don't really understand this argument. Why is a "ref to the wrapper object" (presumably a Function/object identity is a perfectly sensible concept (otherwise we would throw errors when people try to compare them or use them in I'm also not aware of any language that has composite/struct/record/product values but which restricts the type of values that can be composited. Pretty much every modern language either has this feature already, or is moving in this direction. |
@demurgos thats not a capability the language offers, or is likely to offer ever - you can make object wrappers but those simply aren’t primitives in any sense of the word. |
I don't understand this reasoning. Being able to store a function is more-or-less equivalent to being able to store an object, since you can always wrap an object with a dummy function that returns it: function wrap(v) {
return () => v;
}
const obj = {};
const rec = #{ obj: wrap(obj) };
const obj_ = rec.obj(); There doesn't seem to be any difference in "purity" or in creating communication channels between your proposal above and the more obvious proposal of simply allowing objects/functions to be stored, so to me it seems like this proposal is both more complicated and also stifling the equality-based use cases for no reason. Having special behaviour at ShadowRealm boundaries is perfectly sensible but I don't see why it would need wrapper functions or boxes. I'm not familiar with the current ShadowRealm specification, but a user-level implementation of a membrane should be able to treat R/T values specially[0] so that if the R/T value contains an object, the object is replaced with an appropriate proxy (just as if that object were passed through the membrane itself). If the R/T value contains no object, it should be able to detect that easily (using the "predicate to know if a R/T contains a nested function adapt(v) {
if (Object.isDeeplyImmutable(v)) // eg: 4, "foo", #[#{ bar: #["baz"] }]
return v;
if (Tuple.isTuple(v)) // eg: #["foo", {}]
return v.map(adapt);
if (Record.isRecord(v)) // eg: #{ foo: {}, bar: "baz" }
return ...;
// normal logic for proxying objects
....
} This will also allow the original R/T value to be recovered if it's passed back through the membrane to the original realm (though it might be a reallocated version of the same value, like how [0] As a side note, just thought I'd mention that if the membrane doesn't treat R/T values specially and |
Sure, but the fact you have to call the function makes it explicit you're exiting the realm of immutable "static" data.
That is one of the problems with allowing mutable things in immutable structures. Their unforgeable identity matters and becomes part of the equality of the containing R/T. Some people believe that this should only happen with an explicit signal from the author. This is particularly true for objects, which are easy to confuse with records, One could argue that a function is an explicit enough signal that the author intends to include mutable data in a R/T. However as I explained simply using the function object provided by the user is not acceptable as it can be used as a communication channel on its own, which is why I suggested automatically wrapping it. However that causes complications with equality: either you give up on equality matching by always creating new wrappers, or we effectively end up introducing a global (and possibly undeniable) WeakMap from original function to wrapped function in order to provide for stability.
The ShadowRealm proposal is structured in such a way that it's impossible to mix the object graphs of the 2 realms: the callable boundary only allows primitives and callables through, and the latter are automatically wrapped to enforce that only primitives and callables can pass through arguments and return values. It's effectively a small membrane that disallows objects to be shared between the 2 realms. As such you cannot create a proxy and pass it through the membrane as-is (there is a fancy membrane maintained by Salesforce that works with the limitations of the callable boundary, but it effectively relies on causing side effects on shared stack state) |
Right now functions are not allowed in records and tuples, see from the Readme
Excuse my ignorance, but why?
I've been using the R&T polyfill for a while now and continuously bump into R&T not working for a usecase because they're not allowed to contain functions. Object.freeze works on functions, so I'd really like to be able to put them into especially records.
For example I'd want to have an array like this
Where
icon
is a react component, which is a function.That way I could just render this with
And not have to write a comparison function that could break as more properties are added to each step and is far less elegant.
The text was updated successfully, but these errors were encountered: