Skip to content
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

Make computed fields easier to declare #13458

Closed
sethladd opened this issue Sep 20, 2013 · 18 comments
Closed

Make computed fields easier to declare #13458

sethladd opened this issue Sep 20, 2013 · 18 comments
Labels
closed-duplicate Closed in favor of an existing report P1 A high priority bug; for example, a single project is unusable or has many test failures type-enhancement A request for a change that isn't a bug
Milestone

Comments

@sethladd
Copy link
Contributor

I just saw code like this in the wild:

 bindProperty(this, const Symbol('name'), () => notifyProperty(this, const Symbol('statusIcon')));
    bindProperty(this, const Symbol('type'), () => notifyProperty(this, const Symbol('statusIcon')));
    bindProperty(this, const Symbol('value'), () => notifyProperty(this, const Symbol('statusIcon')));
    bindProperty(this, const Symbol('extraValidation'), () => notifyProperty(this, const Symbol('statusIcon')));
    bindProperty(this, const Symbol('minlen'), () => notifyProperty(this, const Symbol('statusIcon')));
    bindProperty(this, const Symbol('maxlen'), () => notifyProperty(this, const Symbol('statusIcon')));

Can we do something better?

Strawman:

computedField(#statusIcon, [#name, #type, #value, ...]); // assuming multiple fields can trigger a change in one field

@jmesserly
Copy link

So pretty soon you could write this in a much cleaner way with Symbol literals:

    notifyStatus() => notifyProperty(this, #statusIcon)
    bindProperty(this, #name, notifyStatus);
    bindProperty(this, #type, notifyStatus);
    bindProperty(this, #value, notifyStatus);
    bindProperty(this, #extraValidation, notifyStatus);
    bindProperty(this, #minlen, notifyStatus);
    bindProperty(this, #maxlen, notifyStatus);

Or even better:

    [#name, #type, #value, #extraValidation, #minlen, #maxlen].forEach(
        (x) => bindProperty(this, x, () => notifyProperty(this, #statusIcon)));

There's still a few problems in this code:

  1. bindProperty is a bad name. It must be renamed anyway because it conflicts with a Polymer thing (oops). Let's call it onPropertyChange.
  2. bind/notify pattern is annoying.
  3. the requirement to supply "this" is annoying. How come I can't use implicit this? Let's say onPropertyChange, notifyPropertyChange are directly on the Observable mixin.
  4. Writing a getter that depends on so many properties is probably a bad idea. Better to listen on the values and cache the computation in a field:

[#name, #type, #value, #extraValidation, #minlen, #maxlen].forEach(
    (x) => bindProperty(this, x, computeStatusIcon));

computeStatusIcon() {
  statusIcon = ...;
}

As for computedField: it's nice in this example, but I don't know how well it scales to other examples. What if some of those properties come from other objects? That's fairly common at least in Polymer. They make it easy to forward a property from a nested component to your outer component (amusingly, they call this "bindProperty" :) ).

Let's instead make onPropertyChange able to listen on a list of names. Now we could get to:

    // note: both of these are calls to "this.", so it works for other objects too.
    onPropertyChange([#name, #type, #value, #extraValidation, #minlen, #maxlen],
        () => notifyPropertyChange(#statusIcon));

Now we're within getting close distance of computedField, but with the ability to listen on or notify on other objects.

Unfortunately there's still a big flaw here. If what we really have is data paths, single Symbols like above won't work. We'd need something more like:

    onPathChange(['entity.name', 'type', 'value', 'extraValidation', 'bounds.min', 'bounds.max'],
        () => notifyPathChange('statusIcon'));

Oy!

You might wonder why we use Strings for paths instead of something else? That's a fair question. The Symbol/String dichotomy is really hard to deal with when many of our names come from HTML, and unfortunately it landed just before MDV did. Now that we have Symbol literals, something like this is more feasible, and worth considering a change to PathObserver:

    [#entity, #name]

That would bring us to something like:

    onPathChange([[#entity, #name], #type, #value, #extraValidation, [#bounds, #min], [#bounds, #max]],
        () => notifyPathChange(#statusIcon));

folding the on+notify together gives us something very close to your original:

    computedField(#statusIcon, [[#entity, #name], #type, #value, #extraValidation, [#bounds, #min], [#bounds, #max]]);

I wonder if it should also be expressed at the meta-level. It seems less error prone to declare this right at the getter:

    @­ComputedField(const [const [#entity, #name], #type, #value, #extraValidation, const [#bounds, #min], const [#bounds, #max]])
    get statusIcon => ...;

Another option is just to expose PolymerExpressions conveniently, and use those:

    get statusIcon => const PolymerExpression('... computation of StatusIcon here ... ').value;

Personally, I don't know if any of this is very good. It is the downside of using observers, and having neither read barriers nor statically analyzable expressions. But such is the world we live in.

@jmesserly
Copy link

fyi -- thinking on this more, definitely leaning towards @­ComputedField. All of the other approaches suffer from the problem that the dependencies aren't localized near the getter.


Removed Priority-Unassigned label.
Added Priority-High label.

@jmesserly
Copy link

also, once my Polymer Core change lands it will be easier at least:

   [#name, #type, #value, #extraValidation, #minlen, #maxlen].forEach(
        (x) => bindProperty(#statusIcon, this, x);

I think the Polymer bindProperty is meant to follow the Node.bind style of "name, model, path"

@sigmundch
Copy link
Member

Added this to the M8 milestone.

@DartBot
Copy link

DartBot commented Oct 9, 2013

This comment was originally written by @seaneagan


Wouldn't this be valid for methods, not just fields? If a method's return value depends on another property in the model, then how do polymer expressions calling the method, know when they are dirty?

So maybe instead of @­ComputedField it could be something like @­DependsOn :

class FooElement extends PolymerElement with ObservableMixin {
  String salutation;

  @­DependsOn([#salutation])
  getGreeting(String name) => '$salutation $name;
}

@sigmundch
Copy link
Member

We would love to get to this. We still need to do some design work on it so most likely it wont be done by M8.


Removed this from the M8 milestone.
Added this to the Later milestone.

@sigmundch
Copy link
Member

Added Library-Observe label.

@sigmundch
Copy link
Member

Issue #12473 has been merged into this issue.


cc @jmesserly.

@jmesserly
Copy link

Issue #14111 has been merged into this issue.

@jmesserly
Copy link

@5 -- the problem with methods is they're only a PolymerExpressions feature. PolymerExpressions could certainly look for an annotation. But putting the annotation on a method would have no meaning at the package:observer layer

@jmesserly
Copy link

The design I proposed in @­1 has a flaw. It has no way of expressing list reductions, for example filtering a list of items. So... back to square 1.

Polymer's solution for computed fields is to use *Changed handlers and embrace mutable public fields:

    @­observable String firstName, lastName, fullName;

    firstNameChanged() => _computeFullName();
    lastNameChanged() => _computeFullName();
    _computeFullName() {
      fullName = '$firstName $lastName';
    }

... it works, but only inside PolymerElements. And doesn't clearly communicate to API consumers that fullName is recomputed. And recompute won't happen until the microtask runs.

@sigmundch
Copy link
Member

Removed this from the Later milestone.
Added this to the M9 milestone.

@clayberg
Copy link

Removed this from the M9 milestone.
Added this to the 1.1 milestone.

@sigmundch
Copy link
Member

Removed this from the 1.1 milestone.
Added this to the 1.2 milestone.

@DartBot
Copy link

DartBot commented Jan 28, 2014

This comment was originally written by @zoechi


This issue was included in a changelist today. May I ask what's new with this change?

@anders-sandholm
Copy link
Contributor

Removed Library-Observe label.
Added Pkg-Observe label.

@jmesserly
Copy link

so, Polymer has added some Polymer-specific features here that are worth checking out. there's this ObserveProperty thingy:

/// @­ObserveProperty('foo.bar baz qux')
/// validate() {
/// // use this.foo.bar, this.baz, and this.qux in validation
/// ...
/// }

makes it easy to do validation or computed properties.

there's also this proposed change:
https://code.google.com/p/dart/issues/detail?id=16342

going to merge this into that bug, since it would essentially make getters "just work"


Added Duplicate label.
Marked as being merged into #16342.

@sethladd sethladd added Type-Enhancement P1 A high priority bug; for example, a single project is unusable or has many test failures closed-duplicate Closed in favor of an existing report labels May 14, 2014
@sethladd sethladd added this to the 1.2 milestone May 14, 2014
@DartBot
Copy link

DartBot commented Jun 5, 2015

This issue has been moved to dart-archive/observe#40.

@kevmoo kevmoo added type-enhancement A request for a change that isn't a bug and removed type-enhancement labels Mar 1, 2016
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-duplicate Closed in favor of an existing report P1 A high priority bug; for example, a single project is unusable or has many test failures type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

7 participants