-
Notifications
You must be signed in to change notification settings - Fork 60
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
Have you considered using lenses for updates? #19
Comments
There are ways of implementing lenses without having to provide a nest function, because lenses are usually functions and should be composable just as functions are: It makes sense to me that This way, two wonderful things happen:
The only problem would be with array prop lenses, because when used without
|
@kwirke I considered "set" instead of "update" initially (as shown in the bug itself), but there were a few things I ran into:
Here's how composition would compare between the two: // "get" + "update"
function composeUpdate(a, b) {
return {
get: v => b.get(a.get(v)),
update: (v, f) => a.update(v, (u) => b.update(u, f)),
}
}
// "get" + "set"
function composeSet(a, b) {
return {
get: v => b.get(a.get(v)),
set: (v, x) => a.set(v, b.set(a.get(v), x)),
}
} If you notice, the "update" version is slightly simpler but requires an extra level of lambda abstraction. |
Hi @isiahmeadows , I don't think I explained myself well, because I think we agree on this. When I talked about About using thunks, that is also possible with const add3 = x => x + 3
over(compose(.a, .b, .c), data, add3) What I'm saying is that, in the implementations I have seen, lenses are decoupled from get/set/over functions, so they are reusable and only coupled to the shape of the type they are lensing. |
@kwirke
I'm aware, and you could do it that way. In library implementations, it's also way easier to implement this way, but of course, syntax does offer you flexibility in how you present them that libraries don't have. And of course, my suggestion above is just a different syntax for the same thing - But of course, there's a key difference here: I'm aware of literally no prior language (aside from the one I'm privately developing - closed source ATM, sorry!) that integrates lenses into the core language itself. Clojure is about the closest I've ever seen, but that only merges objects and hash maps with arbitrary key types.* Lenses are obviously more general than that. * Python technically does make objects backed by non-string hash maps, but they aren't merged at the conceptual level, so I'm not counting it here. |
I mentioned this in the with syntax section, in (#1 (comment)), but I think that keyPaths are the feature we should actually be pushing for. Specifically, being able to provide multiple keys in computed properties: { ...a, [key1, key2, key3]: 5 } === { ...a, [key1]: { ...a[key1], [key2]: { ...a[key1][key2], [key3]: 5 } } } I always prefer data over special syntax, as it allows you to do something like this: { ...a, [...keyPath]: 7 } This is equivalent to a "set" lense as is, and makes it trivial to implement an update lense in userspace: const updated = update(x,
{
[key1, key2]: previous => f(previous),
[key1, key3]: previous => g(previous),
[key4]: previous => r(previous)
});
// Which is equivalent to having manually typed out the following:
const updated = update(x,
{
key1: { key2: previous => f(previous), key3: previous => g(previous) },
key4: previous => r(previous)
}); Where the implementation of update is: function update(target, updates)
{
if (typeof updates === "function")
return updates(target);
const changes = Object
.entries(updates)
.map(([key, value]) => [key, update(target[key], value)]);
return Array.isArray(target) ?
changes.reduce((array, [key, value]) =>
(array[key] = value, array), [...target]) :
{ ...target, ...Object.fromEntries(changes) };
} You can see a working example of this here: https://runkit.com/tolmasky/key-path-update |
@tolmasky This isn't fundamentally incompatible with that and could be altered to work with it. The general idea is abstracting the concept of a "key" and an "update", so things like // Assuming you add `+:`/etc. operators to properties
const incrementWages = data =>
{...data, ["byCity", @each, 1, @each, "hourlyWage"]+: 1} |
I suppose my only point is that through the const incrementWages = data =>
{...data, ["byCity", @each, 1, @each, "hourlyWage"]+: 1} Isn't that much more terse than: const incrementWages = data =>
update(data, ["byCity", update.each, 1, update.each, "hourlyWage"]: x => x + 1 } Where Without a data-driven |
Not a fan of expanding symbols to also wrap lenses. I'd rather keep that difference a bit more explicit, and if lenses become primitives, I'd rather keep it a separate primitive type altogether.
That's not hard: you just need to nest it in an const eachCityEmployee = {
update: (data, func) =>
update(data, ["byCity", update.each, 1, update.each], func)
}
update(data, [eachCityEmployee, "hourlyWage"], x => x + 1) I did consider a purely library concept of this, but I wasn't sure it would gain enough traction with TC39, so I chose to omit it. |
I think maybe there is some misunderstanding here (potentially completely on my part). My point about serializing keyPaths was in response to the perceived benefits/necessity of a built-in const incrementWages = data =>
{...data, ["byCity", @each, 1, @each, "hourlyWage"]+: 1}
const incrementWages = data =>
update(data, { ["byCity", each, 1, each, "hourlyWage"]: x => x + 1 }); Arguably most the difference comes from the manual definition of the +1 lambda. If we were to be employing any more sophisticated update function, they'd truly be nearly identical. This was really my only point with all that. That being said, I wasn't 100% sure I understood the update wrapping (As it doesn't work with my |
Partially, but not completely. 😉
The way you're explaining it, it seems to line up like this: // Yours
{ ["byCity", each, 1, each, "hourlyWage"]: x => x + 1 }
// Userland
data => update(data, nest("byCity", each, 1, each, "hourlyWage"), x => x + 1) The object literal could be desugared to just multiple
My function update(obj, view, func) {
if (view != null && (
typeof view === "object" || typeof view === "function"
)) {
return {...obj, [view]: func(obj[view])}
} else {
return view.update(obj, func)
}
}
I'm similarly focusing on the former, and have specifically refrained from bringing up "built-in" lenses aside from syntactic support and the minimum required to integrate existing properties with them (which I specify as special cases, not actual lenses). |
Update: maybe tack on I'll eventually centralize this all into a gist. Edit: One concrete application is making stuff like React Flare's event handlers feel basically like native properties despite them being more or less specified in userland. You could also have an |
Could you summarize the motivation for using lenses here? I took a stab at getting this started: Our goals with this proposal are given in the overview.
|
This is pretty accurate.
The desugared syntax will itself always accessible. Lenses as proposed here would just be simple With functional programming, where engines really struggle are when you're returning functions, not simply receiving them. It's stuff like It wasn't clear iniitally because it took the process of filing the bug and answering a few questions to figure it out, but my precise proposal currently can be summarized as this:
|
Thanks! It's sounding like the main potential advantage of lenses is ergonomics when doing complex transformations.
Are there apples-apples comparisons between lenses and the current proposal where lenses have especially good ergonomics?
…________________________________
From: Isiah Meadows <[email protected]>
Sent: Friday, August 16, 2019 8:34:16 AM
To: rricard/proposal-const-value-types <[email protected]>
Cc: Max Heiber <[email protected]>; Mention <[email protected]>
Subject: Re: [rricard/proposal-const-value-types] Have you considered using lenses for updates? (#19)
@mheiber<https://github.com/mheiber>
I suspect this has something to do with more complex transformations in deeply-nested data, but don't know enough about lenses.
This is pretty accurate.
efficient cloning and comparison are primary goals of the current proposal. Can lenses be implemented efficiently? I'm worried that if they are syntactic sugar for a ton of function calls that might be more difficult to optimize, but I'm a little out of my depth here. I'm a little unclear on what notion of 'sugar' is at play here. Would the desugared syntax be available as well as the sugary one?
The desugared syntax will itself always accessible. Lenses as proposed here would just be simple {get: (value) => result, update: (value, func) => updated} objects. Engines can of course optimize that very effectively. The main issue is around heavier use of temporary functions, but in my experience, those aren't as expensive as people often believe.
With functional programming, where engines really struggle are when you're returning functions, not simply receiving them. It's stuff like map(func)(list) where map = f => xs => ... or the module factory pattern of (...args) => ({...methods}) that they tend to struggle to properly inline and optimize stuff. My proposal here doesn't hit those problem areas.
________________________________
It wasn't clear iniitally because it took the process of filing the bug and answering a few questions to figure it out, but my precise proposal currently can be summarized as this:
* value.@key → key.get(value)
* value with .@key = value → key.update(value, () => value)
* value with .@key by expr → key.update(value, expr) (note: by is unambiguous here)
* value with .key by expr → value with .key = (expr)(value.key)
* value with [key] by expr → value with [key] = (expr)(value[key])
* value with [email protected] by expr → value with .one by (a) => two.update(a, (b) => b with .three by expr)
* value with .@one by foo, .@two by bar → two.update(one.update(value, foo), bar)
* .@key is specifically just `.` `@` Identifier Arguments[opt] | `@` `(` Expression `)`.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub<#19>, or mute the thread<https://github.com/notifications/unsubscribe-auth/ABDZJFJHYLYDPBHH3YX3K5DQEZJ7RANCNFSM4HVXAY6Q>.
|
@mheiber In the initial comment, here's an example: // Current proposal
const incrementWages = data =>
data with .byCity = data.byCity.map(
([city, list]) => #[city, list.map(
item => item with .hourlyWage += 1
)]
)
// My suggestion here
const each = {update: (list, func) => list.map(func)}
const incrementWages = data =>
data with .byCity.@each[1].@each.hourlyWage += 1
// Desugaring
const each = {update: (list, func) => list.map(func)}
const incrementWages = data =>
data with .byCity = each.update(data.byCity,
(pair) => pair with [1] each.update(pair[1],
item => item with .hourlyWage += 1
)
)
// After JIT inlining - basically the same as the current proposal
const incrementWages = data =>
data with .byCity = data.byCity.map(
(pair) => pair with [1] = pair[1].map(
item => item with .hourlyWage += 1
)
) Makes nested updates inside things other than simple single const value members much easier and less boilerplatey. |
Thanks for summarizing! Could the lense syntax work as a follow-on proposal? I'm hoping the const value types proposal will be bite-sized, but it would be neat if we didn't rule out future ergonomics things. |
Wouldn't be against it, provided the main proposal doesn't prevent it from happening. |
We removed |
For those who aren't familiar with lenses:
I know this sounds a bit like I'm getting ready to suggest some obscure functional programming concept, but I'm not.
Lenses are simple
{get(o): v, set(o, v): o}
pairs. They are really nice for operating on nested data without having to deal with all the boilerplate of the surrounding context. They are easy to write and easy to compose.That
modify
operation is really where all the power in lenses lie, not really theget
orset
.Edit: Forgot to double-check some variable names.
Edit 2: Fix yet another bug. Also, make it clearer which edits apply to this section.
Edit: clarity, alter precedence of function call vs lens
Edit 2: Update to align with the current proposal
Lenses are pretty powerful and concise, and they provide easy, simple sugar over just direct, raw updates. But it kind of requires revising the model a bit:
object.@lens
object with .@foo.@bar.@baz = value, ...
object with .@foo.@bar.@baz prev => next, ...
.prop
instead of.@lens
[key]
instead of.@lens
.@foo(1, 2, 3).@bar("four", "five", "six")
and so on. The functions would be applied eagerly before actually evaluating updates. However, member expressions like@(foo.bar)
need parenthesized.object with [email protected][baz] prev => next, ...
.Of course, I'm not beholden to the syntax here, and I'd like to see something a little more concise.
For a concrete example, consider this:
With my suggestion, this might look a little closer to this:
If you wanted to push or pop in the middle, you could do this:
Lenses would have to have
get
andset
, but if you make theset
to always just an update function, you could also make it a little more useful and powerful. You could then change it to this:object.@lens
↔lens.get(object)
object with .@lens by func
↔lens.update(object, func)
object with .@lens = value
↔object with .@lens by () => value
object with .@lens += value
↔object with .@lens by prev => prev + value
object with .@lens.@lens
,object with .@lens.@lens = value
, etc.{get(object), update(object, ...args)}
.key
and[index]
can be used in place of a lens as sugar for using the lens@updateKey("key")
and@updateKey(index)
object with .key by update
,object with .key = value
object with [index] by update
,object with [index] = value
updateKey(key)
here is an internal lens that returns the rough equivalent of{get: x => x[key], update: v => ({...v, [key]: value})}
. This is not exposed to user code, and simple property access could be altered to be specified in terms of this.x.@lens = value
andx.@lens(update)
as procedural operations.And of course, it'd work similarly:
However, the real power of doing it that way is in this:
Here's what a transpiler might desugar that to:
The text was updated successfully, but these errors were encountered: