Skip to content

Rough Ideas: Promoting final fields. #3358

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

Closed
hamsbrar opened this issue Sep 18, 2023 · 4 comments
Closed

Rough Ideas: Promoting final fields. #3358

hamsbrar opened this issue Sep 18, 2023 · 4 comments
Labels
feature Proposed language feature that solves one or more problems field-promotion Issues related to addressing the lack of field promotion

Comments

@hamsbrar
Copy link

hamsbrar commented Sep 18, 2023

Below are some rough ideas based on my current understanding of the situation(I just skimmed through some discussions related to the topic. In case I've missed something obvious, I'll be happy to get educated about it.)


There are currently three type of getters:

  1. Idempotent: a getter that return same value, on every subsequent call.
  2. Reliable: a getter that return same or a better value, on every subsequent call.
  3. Unreliable: a getter that return a value that isn't better, on some subsequent calls.

A better value b is a value that either implements previous value p or p is null.

Example, idempotent final field/getter:

class T1 {
  final int? g; // is idempotent
  T1(this.g);
}

var i1 = T1(1);
i1.g; // is idempotent

var i2 = T1(null);
i2.g; // is idempotent

Example, reliable final getter:

class T2 extends T1 {
    @override
    int? get g => {
        if (randomBool()) {
            return 1;     // int
        }

        return 2;         // int
    }
}

var i1 = T2(1);
i1.g; // is reliable, always return int

var i2 = T2(null);
i2.g; // is reliable

Example, unreliable final getter:

class T3 extends T1 {
    @override
    int? get g => {
        if (randomBool()) {
            return null;      // null
        }

        return 2;             // int
    }
}

var i1 = T3(1);
i1.g; // is unreliable, it might return null after int and null isn't better

More examples,

  • Getter g1 returning 1(on first call), 1(on second call), 1(on third call) and so on, is an idempotent getter.
  • Getter g2 returning null, null, 1, 1, 2, 3, 0... is a reliable getter.
  • Getter g3 returning null, 1, 2, null... is an unreliable getter.
  • Getter g4 returning null, 1.0, 1... is an unreliable getter.
  • Getter g5 returning null, 1, "a"... is an unreliable getter.

I believe that final fields should always be idempotent because final variables are idempotent(for reads). Overriding a final field with a getter is correct as long as getter is idempotent.

Approach 1: Idempotent final fields

This approach propose that final fields are idempotent by default. This means that users are allowed to override or implement a final field with a getter as long as it's idempotent. Using a getter that is either reliable or unreliable is an error.

Give users the modifier idempotent that they can use on a getter that they believe is idempotent but compiler cannot prove that. This will be a promise that users are going to make just like they make in case of late fields.

class T1 {
    final int? g;
    T1(this.g);
}

class T2 extends T1 {
    @override
    idempotent int? get g => {
        var one = 1;
        return one + get1();
    }
}

dynamic get1() => 1;
  • Idempotent final fields can be cached.
  • Idempotent final fields are subject to promotion.

Idempotent final fields are restrictive and they might no be as useful as I think they're especially since methods used to prove idempotency aren't good and this results in unneccessary friction. Below is a slightly less-correct but better approach.

Approach 2: Reliable final fields

This approach propose that final fields are reliable by default. This means that users are allowed to override or implement a final field with a getter as long as it's reliable. An idempotent getter is also a reliable getter so those are allowed as well. Using a getter that is unreliable is an error.

The easiest method to determine whether a getter is reliable is to make sure that it's returning one type of object(no type union). I believe this will address most of the cases.

Give users the modifier reliable/stable that they can use on a getter that they believe is reliable but compiler cannot prove that.

class T1 {
    final int? g;
    T1(this.g);
}

class T2 extends T1 {
    @override
    stable int? get g => {
        if(true as dynamic && false as dynamic) {
            return null;
        }

        return 1;
    }
}

Reliable final fields are subject to promotion.

Approach 3: Unreliable final fields

This is the current situation.

The situation can be improved using an optimistic promotion at package level iff every getter in hierarchy(that is known to package) is either idempotent or reliable. Then compiler can pull it off using cache at sites where a final field is promoted. Going through each package, if not every getter is idempotent(i.e some are reliable) in hierarchy(that is known to package), then add logic that update the cached value before each read iff new value is better. If some authors aren't satisfied, give them a lint avoid_reliable_getters and an anontation @pragma('idempotent') that they can use on getters that they believe are idempotent and compiler can then optimize them.

If someone need a truely unreliable field, they can just change final to var.

@hamsbrar hamsbrar added the feature Proposed language feature that solves one or more problems label Sep 18, 2023
@eernstg eernstg added the field-promotion Issues related to addressing the lack of field promotion label Sep 19, 2023
@eernstg
Copy link
Member

eernstg commented Sep 19, 2023

Perhaps you could use a different word to indicate "same or better"? The stable getters as proposed in #1518 do indeed have the property that if you evaluate such a getter twice on the same receiver then you will receive exactly the same result (they are identical).

It is true that 'idempotent' is often used to indicate that you get the same result and there are no side effects. Stable getters do not make any such commitment, so you could have three buckets (idempotent getters, stable getters, other getters), but none of them would have this "same or better" behavior.

Also, I consider the ability to reason about the code as crucial, which means that I wouldn't know how to characterize "better" for the general case. If you are running an algorithm that must terminate within 500ms then a new max time of 600ms is different, and it might be better, but I'd prefer to just note that it is not the same, and the algorithm will have to include som user-written code to determine whether it's better or worse, and what to do about it.

By the way, I like your characterization of the current situation as "unstable final fields". ;-) It is indeed true that we have a keyword final that developers can put on certain instance variable declarations, but they are completely useless for clients because the clients can't assume anything about that property beyond "the interface has a getter".

@hamsbrar
Copy link
Author

:)

Done.

  • Renamed Stable to Reliable (for promotions).
  • Renamed Unstable to Unreliable.

Also, I consider the ability to reason about the code as crucial, which means that I wouldn't know how to characterize "better" for the general case.

I see I didn't mention it properly. An idempotent getter allow code that is subset of code allowed by a reliable getter. A reliable getter allow code that is subset of code allowed by an unreliable getter. An unreliable getter allow code that is currently allowed. So I'd see any approach/concept as 'better' if it can promote getters and ensure type soundness while breaking as little code as possible.

@hamsbrar
Copy link
Author

hamsbrar commented Sep 20, 2023

Also, this is just a thread I've used to share ideas because I didn't want to hijack #3307(where this conversation begin). No idea why people are disliking it(maybe it's easy or maybe it's the only thing they can do. I hope they'll write and enlighten me).

I've changed stable to reliable not because stable doesn't fit the description but because I don't want anyone to confuse 'stable' with eernstg's 'stable'. I'd still go with 'stable' in practice i.e 'reliable' is used for mere explanation. It's the term 'impure' that is assumed when 'pure' is omitted because it's more general i.e it covers cases that are pure. If eernstg's getters allow side effects then they are 'impure idempotent' else they are 'pure idempotent'(one can call them just 'pure' as well). To me, 'stable' means something can't get worse but it can defintely get better. A getter returning stable values mean one can promote them and every subsequent value will be same or better for promotion(which means it won't invalidate the previous promotions but can allow more promotions).

@hamsbrar
Copy link
Author

If ideas expressed in this thread are sound, then this thread could serve as an inspiration for a more concrete proposal that uses the right keywords at right places, and then people can vote as they should.

I'm closing this because I lack the motivation to do that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems field-promotion Issues related to addressing the lack of field promotion
Projects
None yet
Development

No branches or pull requests

2 participants