Skip to content

Opt-in type promotion - Analysis #4132

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
hydro63 opened this issue Oct 20, 2024 · 10 comments
Closed

Opt-in type promotion - Analysis #4132

hydro63 opened this issue Oct 20, 2024 · 10 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@hydro63
Copy link

hydro63 commented Oct 20, 2024

DISCLAIMER - this is not a feature request, just an analysis of possible feature

Context

Inspiration This issue / thread is based on @lrhn comment on issue #4131, where he said, that for type promotion of immutable members of class to be possible, it would need to be opt-in (not language-wise), to make sure that the developer understand what he is doing, etc ... He talks about the developer needing to make a *promise*, that he won't do unsafe stuff, and as such, it can't just be implemented language-wise.

This issue is trying to create a way for a developer to make a promise, and allow the developer to specify, when a certain thing is promotable, despite the compiler disagreeing. It is kind of like with null assertion !, where you say to compiler that you are sure that it's not null.

Motivation

From what i've seen, significant portion of the issues / feature requests posted to the language repo talk about type promotion in some specific edgecase, when the developer is certain that it can be promoted without causing any trouble. This feature would make type promotion easier and faster to use, which would in turn make those developers at least a bit happier.

In current Dart, there are already mechanisms to do a type promotion - inline casting or assigning to new variable. The problem with both is that both are quite verbose and redundant, since we already know what type a variable is.

if(foo.prop is String){
  final prop = foo.prop as String;
}

// okay with one prop, more difficult with multiple
if(foo.prop case var prop?){

}

This feature would ideally make it easier to do explicit type promotion, and remove a lot of the bloat associated with types.

Issue in practice

Here is how the issue would work in practice:

class Foo {
  int? prop;
  ...
}

void func(Foo foo) {
  if(foo.prop == null) return;

  // without promise - current behaviour
  // foo.prop != null, type => int?

  // developer promises that it can be promoted (doesn't have to be this syntax)
  promote foo.prop;
  // foo.prop != null, type => int;
}

Possible syntax

Here are the possible syntaxes i thought of, that would have the function of making a promise, that something can be promoted.

Annotation on member / field / variable

A specific variable would be annotated with the promise during it's definition

class Foo {
  @promotable
  final int? prop;
}

void func(Foo foo) {
  if(foo.prop == null){
    print("NULL");
    return;
  }
  foo.prop;    // type => int
}
+ allows negative promotion (foo.prop == null -> return ==> foo.prop != null)
- delegates managing the promotion, to the interface not the user. (REALLY BAD)

Annotation on block

The promotable variables would be listed before the block they were defined in.

class Foo {
  final int? prop;
}

@promotable(foo.prop)
void func(Foo foo) {
  if(foo.prop == null){
    print("NULL");
    return;
  }
  foo.prop;    // type => int
}
+ promotion semantics are responsibility of the user, not the interface
+ allows negative promotion
- separation of type behaviour and the code behaviour
- possible problems with variables with the same name

promote condition

The promote condition would work the same as the if condition, while also signifying the promise of promotability. If the condition fails, the content in the block will not be executed, nor will it throw. Instead it will work like a regular if condition, and execute the statement after it.

void func(Foo foo) {
  promote(foo.prop != null){
    foo.prop;    // type => int
  }

  print("NULL");
}
+ clear lifespan of promotability
+ clearly defined conditions of promotion
- doesn't allow negative promotion (since the promotability is defined only in the block)
- maybe we don't wish to promote every variable that is in the condition
- promote would need to be a keyword

promote statement

The promote statement would try to promote everything, that the user specifies. The specified fields / variables would be primarily promoted to their inferred type, with optional explicit typing allowed. The types would be inferred from the information leading up to the promote keyword, and the types would be promoted until the end of the block. If it can't promote, it will throw.

void func(Foo foo) {
  if(foo.prop == null){
    print("NULL");
    return;
  }

  promote foo.prop;
  // type => int - compiler inferred the type
  
  if(foo.prop is Bar){
    promote String foo.prop.dynamic_union;   // if not String -> throws
  }
}
+ explicit listing of fields to be promoted
+ allows union (implemented as dynamic) type assertion
+ allows negative promotion
+ allows sealed type promotion
+ allows explicit typing
- promote would need to be a keyword

Conclusion

I think that if this opt-in promotion would be ever implemented, or even taken seriously, i think that the last option (promote statement) is the most viable solution and the best and most flexible syntax. It would allow both quick return / throw, if the types of the arguments don't match, and also quick promotion for the variables we are sure can be promoted safely.

Still, as i said, this is not really a feature request, it's a feature analysis, so i'd appreciate your ideas for the possible syntaxes, or critisisms why it would be bad.

@hydro63 hydro63 added the feature Proposed language feature that solves one or more problems label Oct 20, 2024
@lrhn
Copy link
Member

lrhn commented Oct 20, 2024

Using annotations won't be the way, they're traditionally not allowed to change language semantics.

See #1518 for a possible syntax for ensuring that a getter is stable, and it's value can therefore be assumed to be the same between two different reads.
If that is guaranteed by the author, the variable can safely be promoted by the language.

The other two, more local, approaches require that the two reads of foo.prop have the same value, which nothing otherwise guarantees. That means that the second foo.prop must not read the getter again, but should instead reuse the value that was read the first time. The same way patterns cache property values to ensure that two tests on the stand priorty are years on the same value, the attempt to promote a property would treat that property like s local variable initialized with the real property value only the first time it's read.

That's what a pattern check with binding does explicitly, introduce a local variable.
Which means it's really the same thing we do today, just treating foo.prop as the name of an implicit local variable.

I think it's unlikely that Dart will allow normal code to look like foo.prop but actually does something else. It's too confusing.

On the other hand, if the foo.prop getter is guaranteed to return the same value and have no side effects, then not reading it twice becomes an optimization, and any test done on the first read can be adults to also hold for shower reads. So the stable getter approach could work.

@hydro63
Copy link
Author

hydro63 commented Oct 20, 2024

This issue is not trying to change the behaviour of getters or implicit type promotion. This issue is specifically about "what if i know that it can be promoted without sideeffects", where the user overrides the compiler issues with the promise that it won't cause a problem. Behaviour-wise it is similar to null assertion, which overrides the compiler telling it, that it can be null, despite the user knowing full well it's not.

In other words, imagine if Dart didn't have null assertion operator, and you would need to assign new variable / cast it every time you wanted to use it as non-null value. This is the current behaviour with advanced type promotion, and this issue is about adding a sort of "promotion assertion" construct, sort of like null assertion

The promotion would still check the types of the values and would probably (depending on specification) throw if the types don't match.

This means that instead of user requesting from the language team creation of type promotion exception just for one very specific edgecase, and not being able to use it till the next patch, the user would just tell the compiler "trust me", and the compiler would promote it if it can (based on looser rules)

@ghost
Copy link

ghost commented Oct 20, 2024

The promotion would still check the types of the values and would probably (depending on specification) throw if the types don't match.

The promotion that inserts type checks is not a promotion, You need another word for this. It would be easier to write foo.prop! explicitly than to tell the compiler "Whenever I write foo.prop below, insert an exclamation mark automatically".

This issue has been discussed for years. If you ask me, there's no good solution other than reinterpreting "final" to mean (by default): this thing stays final in subclasses. This is a potentially breaking change—there's no appetite for this at the moment. Other solutions are too complicated, IMO.

@hydro63
Copy link
Author

hydro63 commented Oct 20, 2024

@tatumizer you've misunderstood me. What i meant was it would do type checking when promoting, after that it promote the variable. It won't check how the variable is used.

I've said this because i am in favor of the last syntax, which would allow promoting with explicit type, and so it would need to check if it can be promoted to the explicit type, not just the inferred one.

promote String foo.value;

Also, my proposal is not JUST about promoting nullable type, but also about promoting any sealed typed implicitly or any type explicitly.

@ghost
Copy link

ghost commented Oct 20, 2024

@hydro63 : your clarification doesn't make any difference.
While handling the instruction
promote String foo.value;
the compiler will ask itself: "is it really safe to treat foo.value as String from now on?"
And answer: "I can't see any evidence for this"
It can't see any evidence because ... there's no evidence!
True, sometimes there is evidence - e.g when you access local variables - but in this case, the compiler will promote even without your advice.
For class members, we need a guarantee. There should exist some enforceable (and enforced) property of the program that ensures that the promotion is safe. One example of such property would be immutable qualifier on the class (not an annotation!). The same could be done for a specific field, like `immutable String? foo="hello"; - hence the "stable" proposal (you can search for it).
Sealed or final classes impose some restrictions, but they are insufficient.

@hydro63
Copy link
Author

hydro63 commented Oct 20, 2024

Exactly, since there is no guarantee, the compiler can't promote it. But what if there is information that the user knows, and the compiler doesn't, like for example, that the user won't be stupid and won't try to override final variables.

What i want is to able to say to compiler that, i know there is no guarantee, but I guarantee it. I will be the guar​an​tor, and so you can just promote it. If i'm somehow wrong, just let it crash.

@ghost
Copy link

ghost commented Oct 20, 2024

@hydro63 : what you are missing is: the "brittleness argument".
During the lifecycle of the program, another programmer may do something contrary to your guarantees - maybe because the maintainer is unaware of them. Or somebody uses your library and creates a subclass that breaks your assumptions. Who is to blame then?
It's not you, or anybody, but the compiler itself takes responsibility for the type safety of your program.
Sometimes, in some languages, exceptions can be made for performance.
To enable such exceptions, the programmer explicitly marks some parts of the code as "unsafe", which in your case could look like (syntax is made-up):
#unsafe: promote String foo.prop.
Since you don't gain much in performance, such an instruction cannot be justified.
The languages that support unsafe blocks normally provide a variety of unsafe instructions (e.g. "no bounds checks").
Dart is not one of those languages.

@hydro63
Copy link
Author

hydro63 commented Oct 20, 2024

I understand this, but i still think that it could be a good feature if used correctly. Still, thank you for your opinion

@lrhn
Copy link
Member

lrhn commented Oct 21, 2024

It won't check how the variable is used.

But it will. It must. The Dart type system is sound. It's not always safe, meaning that there are things you are allowed to do that are not guaranteed to be type- correct at compile-time. In every such case, the compiler inserts a runtime check that will throw if the result of an expression doesn't have its static type. That makes the language sound in the sense that if an expression has static type T, and it evaluates to a value, then that value has a runtime type that is a subtype of T.

What you are describing is a way for the author to tell the compiler to "trust me, I know what I'm doing".
Dart has that already: Anything typed as dynamic, every as cast, and unsafe covariant generics. Those are all unsafe, and there is a runtime type check.
As @tatumizer says, your feature amounts to telling the compiler to add a ! (or as Foo if it's not just nullability) on every reference to foo.prop from here on.
That's like Kotlin's Foo! type which really means "Foo?, but you can use it unsafely as Foo, and it throws at runtime if it's null."

@hydro63
Copy link
Author

hydro63 commented Oct 22, 2024

Considering that i started this feature analysis on wrong assumptions (aka Dart always does type-checking), and the fact that this feature is really not all that good with corrected knowledge, i think this issue should be closed (unless someone wants to add something)

So i'm gonna close this issue.

@hydro63 hydro63 closed this as not planned Won't fix, can't repro, duplicate, stale Oct 22, 2024
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
Projects
None yet
Development

No branches or pull requests

2 participants