[go: nahoru, domu]

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

The final keyword is too long #136

Open
lrhn opened this issue Dec 17, 2018 · 87 comments
Open

The final keyword is too long #136

lrhn opened this issue Dec 17, 2018 · 87 comments
Labels
request Requests to resolve a particular developer problem

Comments

@lrhn
Copy link
Member
lrhn commented Dec 17, 2018

It's inconvenient that declaring final variables require more typing than non-final variables. This discourages the use of final variables.

Even with inferred types, it's shorter, and if you want to specify the type, it gets even longer:

var foo = ...;
String foo = ...;
final foo = ...;
final String foo = ...;

While final variables are not strictly necessary (if you don't assign to a variable, it doesn't matter semantically whether it's declared final or not), some people prefer a style where you make variables final unless they need to be mutable, and Dart does not support that writing style very well.

See #135 for one suggestion which improves the experience for inferred variables by replacing final with val. It doesn't improve the typed case. It requires adding a new built-in identifier (but likely not a reserved word).

Another option could be using := for final initialization (since final local variables need to be initialized):

foo := ...;
String foo := ...;

This doesn't work for instance variables since they may be intialized by a constructor, or parameters, and to keep requiring the final word for instance variables and parameters, and using := everywhere else, is inconsistent.

@lrhn lrhn added the request Requests to resolve a particular developer problem label Dec 17, 2018
@pschiffmann
Copy link

FWIW, I can report my personal experiences here.

I've been using final inferred variables for some time now (all my repos have the prefer_final_locals and omit_local_variable_types lints enabled). For local variables, the overhead is only two characters, and after getting used to it, I hardly ever notice it.
For class instances the picture is similar due to type inference: either the overhead is only two characters (class C { final m = <String, List<int>>{}; }), or the variable is not initialized on the same line, so there usually is sufficient space for the six addtional characters (class C { final Map<String, List<String>> m; }).

However, there is one place where I would love to use final variables, but can't bring myself to use the keyword because it is just too verbose: function parameters. The additionaly six characters per parameter clutter the signature with keywords that establish a constraint most programmers assume anyways (see the parameter_assignments lint), and will regularly break the dartfmt 80 character limit.
So IMHO, this would be the place that would profit most of a more compact final marker.

lints:

@peldritch
Copy link
peldritch commented Dec 17, 2018

let could be preferable to val or := for immutable bindings:

val implies the rhs is a value object Kotlin style, not just a binding.
val is tricky to distinguish from var when reading/eyeballing code.
let is used in kernel.
let is more pleasant to touch-type on english keyboards than val.
:= is less touch-type friendly.
let result = when { } is aesthetic for expression/functional style programming.

@zoechi
Copy link
zoechi commented Dec 17, 2018

I like final because it stands out.
let would be better than val in this regard.
The distinction between val and var is easy to overlook,
but it's easy to switch between var and val.
Usually readability should have higher priority.

@yjbanov
Copy link
yjbanov commented Dec 17, 2018

I think a bigger problem with final is that it's not the default. I'd rather have to put var to mean that I want something to be, well, variable, and in the case of local variables I'd want the compiler to scream at me if I don't actually mutate it. So:

String foo = readFoo(); // final by default
final foo = readFoo(); // same thing but infers type
var String foo = readFoo(); // foo is mutable
var foo = readFoo();  // type inference

This would also solve @pschiffmann's problem with function parameters.

Also, I agree with @zoechi that let is a better keyword for this.

@mraleph
Copy link
Member
mraleph commented Dec 17, 2018

Note that let is a non-final binding in JavaScript so it can be confusing for people coming from JS.

@peldritch
Copy link
peldritch commented Dec 18, 2018

Dart is enough of a distinct entity to JavaScript that it's usage re let can differ.

Also, JavaScript devs may appreciate a succinct let vs a more verbose and misleading const for immutable bindings.

@peldritch
Copy link

let comes from the functional world and functional concepts are gaining dev mindshare.

So let could be part of a story re introducing more functional and expression orientated elements into Dart.

@Hixie
Copy link
Hixie commented Dec 18, 2018

It'd be neat if you could opt-in to making final the default.

{$FINALDEFAULT ON}
or
#pragma final_default on
or
.ALLFINAL
or
use final;
or
from __future__ import everything_final

@kasperpeulen
Copy link

Swift also uses let and has the same meaning as final in Dart.

@peldritch
Copy link
peldritch commented Dec 18, 2018

Swift let is like val in Kotlin. The rhs is immutable as well as the lhs binding.

Is this proposal about a shorter lhs binding keyword or about a broader story re immutablity?

Personally just think lhs binding is appropriate.

@eernstg
Copy link
Member
eernstg commented Dec 18, 2018

It's about the binding of the new declared name to an object. That object may be mutable. For immutability of instances, check #125.

@yjbanov
Copy link
yjbanov commented Dec 18, 2018

@mraleph I wouldn't be worried about JS semantics at all. Over there the var vs let vs const distinction is already too confusing to let that language inform what Dart should do.

@peldritch Swift does not make the rhs immutable. The following is valid Swift code:

class Mutable {
    var message: String = ""
}

let m = Mutable()
m.message = "What's up?"  // totally OK

// What it doesn't let you do is rebind the variable:
m = Mutable()  // ERROR

This is also the semantic we want in Dart.

@peldritch
Copy link

@yjbanov

Right, Swift let has a rhs effect for struct but not for class. So it's moot given Dart doesn't have structs.

@Zhuinden
Copy link

let result = when { } is aesthetic for expression/functional style programming.

Hold on, can Dart do that? 🤔

@dlepex
Copy link
dlepex commented Jan 2, 2019

I like final because it stands out.

Likes and dislikes are not very important for the sane language design, especially if you cannot justify them.

Most variables are "final", and only minority are vars. It is absolutely illogical for the common case to stand out! But for the rare case it may be a desirable property.

@Zhuinden
Copy link
Zhuinden commented Jan 2, 2019

It might make more sense to rename var to mutable so that people feel bad about writing it down.

(edit: i'm kinda joking, just use let)

@zoechi
Copy link
zoechi commented Jan 3, 2019

@dlepex

I like final because it stands out.

That depends on how much change is possible.

I'd prefer #136 (comment) any time.

especially if you cannot justify them.

I think I did.

@munificent
Copy link
Member

Most variables are "final", and only minority are vars. It is absolutely illogical for the common case to stand out! But for the rare case it may be a desirable property.

There are a few separate concerns here that I think should be teased apart:

  1. Most local variables are not re-assigned.

  2. The author of the code wants to record their intent that the variable cannot be reassigned.

  3. The reader of the code wants to see that intent reflected in the code.

The first point is observably empirically true. But it's not clear to me that the latter two are, at the level of local variables. There are a lot of intents that a programmer could write down for later programmers to know, and some of those could be mechanically verified by the analyzer. For example:

  • We could add an undef keyword to mark the end of a scope where a local variable should disappear. Most locals aren't used all the way to the end of a block, and most agree that the smaller the scope a variable has, the easier code is to understand.

  • We could require users to add some marker when they refer to a variable declared outside of a lambda. Closures affect the lifetime of the object and are "less local", so maybe it would be helpful to force users to opt into it and see that it's happening.

  • We could make array sizes part of their type, like Pascal does. That lets you statically avoid some array bounds errors.

  • We could make a distinction between strings which can and cannot be empty. Lots of code considers it an error for an empty string to be passed, so why not check that mechanically?

With all of these, the question isn't "is the intent useful?" It's whether the intent useful enough to:

  • Ask users to spend the mental effort to decide what their intent is and write it down.
  • Ask all users to understand this feature of the language and be able to maintain code that uses it.
  • Ask all users to spend time reading code that is more verbose because it expresses this intent.
  • Ask users to go back and change their code when their intent changes. For example, if you later decide you do want to assign to some variable, you have to go back and turn the final (or whatever) into var.

For local variables, it's not clear to me that it's actually a net productivity gain to track which ones can be assigned and which can't. The variable is already local, so its scope is relatively small. It's often only a second's glance to tell if it is reassigned. Most editors will show all uses of a variable when you hover over it.

I do wish we had a shorter keyword for single assignment variables. (I pushed for val way back before Dart 1.0. Alas.) It would be particularly nice for fields and top level variables. But, for locals, I honestly don't think using var everywhere causes any measurable harm. It makes it easier when you do want to assign to them, and the fact that it isn't reassigned rarely improves the readability by any noticeable amount.

And, in general, I think it's important that we make a distinction between what the code the user wrote happens to do and what they intend it to be able to do. If I write a class and don't give it a private generative constructor, that doesn't necessarily mean I intend for users to subclass it. I may have simply not bothered to author any intent one way or the other. I think that's likely true of almost all uses of var for local variables.

@Hixie
Copy link
Hixie commented Jan 4, 2019

In Flutter we've been enforcing the use of final everywhere (except for loop variables and arguments, cc @pq) for a while, and I've found it really helpful to know immediately which variables are going to mutate and which are not. Surprisingly so, in fact. It's really confidence-building when reading new code if you can immediately know that a particular variable is not going to change, especially when there's multiple levels of complicated nested loops.

I wish we could opt-in (on a per-file or per-library basis) to making final the default, with var Foo foo = ... to declare a non-final field, removing final from the language.

@natebosch
Copy link
Member

+1 on the value of seeing at declaration time whether a variable may be reassigned. I certainly can scan down a function looking for assignments, but when I'm trying to wrap my head around new code final is a shortcut to understanding that is a huge benefit in the code that uses it.

I do not find it a drawback to need to go back and remove a final (or change a let to a var) when I add a new assignment to a variable - it's a reminder that my change isn't as shallow as I may have thought. And the same argument can be made for how easy this is - since the scope is small I shouldn't need to move far to do it.

@yjbanov
Copy link
yjbanov commented Jan 4, 2019

FYI, just filed related #160 (if immutable shared objects also support sharing closures then the ergonomics of final will be even more important).

@munificent
Copy link
Member

One wrinkle with pushing towards single assignment locals by default is parameters. Even if we get let or val and encourage everyone to use it whenever possible, there's still the question of whether parameters should be single-assignment or not.

  • If we think single-assignment is definitely better, than that's an argument that parameters should be implicitly final.

  • On the other hand, an assignable parameter is strictly more useful than a single-assignment one. If you do want to assign to it, the current behavior already enables that without requiring you to opt in.

Scala, Kotlin, and Swift all treat parameters as single-assignment. They also all had ways to out of that which they then later deprecated and removed so that parameters are only single-assignment. That's a strong signal that it would be the right behavior for Dart if we moved to a shorter local variable keyword and encouraged everyone to use it. However, that would also be a massively breaking change. It's an automatically toolable one, but still. :-/

@Hixie
Copy link
Hixie commented Jan 4, 2019

They also all had ways to out of that which they then later deprecated and removed so that parameters are only single-assignment

Do you have any links to more details on that? I'd love to read about these changes.

@munificent
Copy link
Member

Oops, I may have misspoken about Scala. They may have always been single-assignment. Here is some discussion of the changes in Swift and Kotlin.

@Hixie
Copy link
Hixie commented Jan 4, 2019

Those seem like pretty compelling arguments, I'm sold.

@davidmorgan
Copy link
Contributor

The IDE could use syntax highlighting to tell you whether a variable is reassigned, with no help needed from the developer.

This is trivial to implement--I've done it--but unfortunately it requires a new version of the analyzer<-->IDE protocol, so it's breaking change, and the analyzer team wanted to wait for more new features to justify making a breaking change. As far as I'm aware there haven't been any others in ~years, though, so maybe it's time.

@Hixie
Copy link
Hixie commented Jan 7, 2019

IDE's don't help when you're trying to understand the code in a YouTube video or on a slide at a conference or whatnot.

@cedvdb
Copy link
cedvdb commented Nov 2, 2021

my proposal was intended to match the current status quo, with just the default behaviour flipped. Today you can't say var String a = 'str'.

An analyzer should check the percentage of final vs var (in all its forms, without considering params) that could give some hindsight to see if that makes sens. I personally think it makes sens to be restrictive by default and final String is way too verbose.

@cedvdb
Copy link
cedvdb commented Nov 3, 2021

Also, this opens a way to using two styles of declarations: one with the explicit final, another - with final by default:
final int x=42;
final x=42;
Which is not very good

This is already how it works?

But these two notions ("final" and "type") are totally different.

The point is that writing final String x or final x is supposedly more widely used than either var x or String x. While the two first expressions are used way more than the two later, both are more verbose. Switching the default would give:

final x = 'final';
String x = 'final';
var x = 'var';
var String x = 'v'ar;

@willhaslett
Copy link

I'd be fine with leaving final for that specific case of declaring a variable without a type (same as var today).

I think this misses the point, which is that many want the convenience and shorter notation of var, with the benefits of final by default.

My understanding is that Flutter prefers LHS types , but google3 enforces omit_local_variable_types, so ideally we could land on a solution that satisfies both parties.

@leonsenft, tangential to your point, but I find it odd that omit_local_variable_types uses the word "usually" in its rationale. Is it that the worst, rare case is that type inference is doable but expensive? If it can ever fail...

@leonsenft
Copy link

@willhaslett

Is it that the worst, rare case is that type inference is doable but expensive?

No, it's because sometimes inference will give you a type you don't want - for example one that's too specific:

abstract class Node {
  Node? get parent;

  Node? ancestorThat(bool Function(Node) matches) {
    var current = this; // Inferred as `Node`.
    while (current != null) {
      if (matches(current)) {
        break;
      }
      current = current.parent; // Error; can't assign `Node?` to `Node`.
    }
    return current;
  }
}

One solution here is to widen the type of current with a type annotation to allow null:

    Node? current = this;

@ykmnkmi
Copy link
ykmnkmi commented Nov 4, 2021

@leonsenft same cases, sometimes I'm looking for feature like this

var? current = this;

@Levi-Lesches
Copy link

I don't think we should make non-final look more confusing than it is today. Final by default, meaning a few extra letters for non-final, is fine, but I don't see the value in a punctuation symbol over var or String.

@Wdestroier
Copy link

I'd say final is fine... It is only annoying in Java, because you need to write final var x = ...;
Making the var keyword final is strange, it would define a variable that isn't variable!
And the programmer would need another keyword to define a variable that is variable.
val vs var is too easy to miss when reading the code and let is kind of meaningless...

@mateusfccp
Copy link
Contributor

Making the var keyword final is strange, it would define a variable that isn't variable!

This isn't true... A final variable is not immutable, it's only not reassignable. You can still, for instance, make a final a = <int>[] and then call a.add(0). The final variable will vary.

@Wdestroier
Copy link

The final variable will vary.

The variable won't vary, what will vary is the class instance field value.

class Text { // Text is a class that explicitly extends
  // the Object class, thus it is an object.
  String value; // value is a field, String is the field type.

  Text(this.value);
}

void main() {
  // name is a variable.
  var name;
  // new Text is a class instance
  name = Text('Gates');
  
  // The Text class instance is not immutable.
  // Because it contains fields that can have their value changed.
  (name as Text).value = 'Steve';
}

Take as example the following line, which is valid code in Dart:

var name;

This line declares a variable reference called "name". This variable may reference a mutable or an immutable object instance.
Example:

var name = const Text('John'); // the var keyword is not related to what it references.

If the var keyword was final, then the line above would declare a final reference to an immutable object. Nothing would be variable at all.

@lrhn
Copy link
Member Author
lrhn commented Dec 22, 2021

Depending on context, the word "immutable" often carries connotations of deep immutability.
I think it's fair to point out that final variables are not that kind of immutable. They're what we have elsewhere called "unmodifiable". A const variable is deeply immutable, on the other hand. (Well, structurally, not necessarily logically, if it pretends to have mutable state using expandos).

That said, we're talking about Dart final variables here, and their meaning is well defined.

@Squinnb
Copy link
Squinnb commented Apr 6, 2022

It would be nice even just to shorten final to fin.

@He-Pin
Copy link
He-Pin commented Apr 6, 2022

If Dart would go with val and var as Scala does, that would be very nice I think.

@cedvdb
Copy link
cedvdb commented Apr 6, 2022

val is too close to var lexically.

I don't think it needs to be shortened personally, it's quite expressive as is. I'd be fine with it be the default but it does not really matter to me except for parameters.

tangential to your point, but I find it odd that omit_local_variable_types uses the word "usually" in its rationale. Is it that the worst, rare case is that type inference is doable but expensive? If it can ever fail...

Maybe for cases where you do want super type. eg Pattern x = '...' instead of var x = '...'' because you might reassign. In the context of final this does not make sense.

The case where I use both is with late, and it's quite long indeed late final Widget

@gintominto5329
Copy link
gintominto5329 commented Jan 13, 2023

Hello,

I think dart 3 is a good opportunity to move to "final by default",

And instead of promoting dart 3, as a mere null-safety release, we would be eligible for the full fledged "safer language" promotion,

Also i read in some issue (on any one of dart/sdk or dart/lang or flutter, repo)(im unable to find it now), that

Dart is promoted as a client optimized language

Which does NOT sound like great, and actually makes the first impression, maybe not bad, but less good, than something like "Safer language"(a claim, backed by final-by-default, and sound-null-safety),

just my opinion,
thanks

@gintominto5329
Copy link
gintominto5329 commented Jan 13, 2023

Also, I have a question,

Why cannot, const, and final; be made automatic, because the analyzer knows, that

  • the called constructor is const or not, and
  • the re-assignment of variables,

thanks again,

@lrhn
Copy link
Member Author
lrhn commented Jan 13, 2023

Too late for Dart 3 to add another major feature.

I too would vote for "final by default", as I've suggest up-thread, but it's not a small change. It requires migrating all code in the world. (Again!)
It should be completely automatable, at least, unlike null safety, which also suggests that it doesn't actually change anything except the default. There is no new power, no new protection, like there was with null safety. That lowers the benefit in the cost/benefit ratio.

Making final automatic when a variable is not assigned to is ... basically status quo. If you add an assignment, the variable would stop being final. Whether the variable is final or not should not have any effect if there is no assignment. You can't distinguish "implicitly final if not reassigned" from just plain var.

Making const automatic when calling a const constructor with constant arguments is possible, but has some sharp edges. First of all, it doesn't matter for anything except canonicalization of constant values. There is no discernible semantic difference between a new ConstConstructor(constArgument) and const ConstConstructor(constArgument) other than the identity of the object - the latter is identical to other similar const invocations. The real difference is memory consumption or churn, if you create a lot of new objects.

Also, whether const can be omitted gets tricky around collection literals.

var c1 = ConstantConstructor(const []); // Would be implicitly `const`?
var c2 = const ConstantConstructor([]); // Is explicitly `const`.
var c3 = ConstantConstructor([]); // Would not be implicitly `const`, because the argument is not constant.

It would be easy to take a big constant structure like var c = const Something(Big(Long(Deep(42, [], "banana"))); and think you could remove the const. Or have a big structure that is implicitly const, forget that it's not a const context, and add an empty list like [] somewhere, and suddenly the entire structure becomes non-constant.

And you wouldn't notice, because it still runs, it just allocates a lot more, mostly indistinguishable, objects.

We considered this at some point, and decided that the risks were too big, and it's better to make you always write at least one const when you want to create a constant object. It's worse if you must write it sometimes, and don't have to in other cases, and the only difference can be a single two-character change deep in a large structure.

@whoizit
Copy link
whoizit commented Feb 6, 2023

final is too long, but I hate :=
this is why I dislike that

let from rust and immutable by default sounds good for me. Or even fin

@myConsciousness
Copy link
myConsciousness commented Feb 27, 2023

Maybe this topic has already changed topics, but based on the title I am writing my opinion.

When I read this title, I thought it would be nice if the variables were final from the beginning, but then I would have to write the type instead of final, forcing even more strange declarations.

On the other hand, I would not like to see strange syntax added. (:=? Please don't!) I like Dart's final declaration very reasonably.

@modulovalue
Copy link
modulovalue commented Feb 27, 2023

If we take this issue to the extreme, I think an argument could be made that other keywords like extension, required and Function are also too long. We could abbreviate extension to ext (6 characters saved) required to req (5 characters saved) and Function to <- (6 characters saved). When should we stop?

Here's perhaps an unpopular opinion: I think that the approach that Dart takes is far superior to all the alternatives proposed in this issue.

  • final is clearer in stating the intended effect that this variable can't be assigned to again because final is semantically much closer to that idea than val, let or :=.

  • mutable by default allows new developers to be productive and make progress much quicker because they are not forced to use concepts that they really don't need.

  • I think that syntax should be optimized for reading and not for writing because code is read more often than it is written. I agree that typing two extra characters can be annoying, but when reading code, the two extra characters are really helpful in finding out what's mutable and what's not.


I agree that the ability to write those keywords quicker would make developers slightly more productive when writing code. But if the goal is to reduce keystrokes, then I'd recommend the pattern that is known as the hyper key on macos (hyper key via karabiner elements / hyper key via better touch tool). It allows you to reassign caps lock to a new modifier. That way you can get your keystrokes down to 2 keys (e.g. hyper+f to type final, hyper+r to type required, hyper+n to type Function and so on).

@rchoi1712

This comment was marked as resolved.

@rchoi1712
Copy link
rchoi1712 commented Jul 22, 2023

Another option could be using := for final initialization (since final local variables need to be initialized):

foo := ...;
String foo := ...;

This doesn't work for instance variables since they may be intialized by a constructor, or parameters, and to keep requiring the final word for instance variables and parameters, and using := everywhere else, is inconsistent.

This would be similar to the approach taken by Go, with short variable declarations. Those are only allowed in function blocks, also.

Thinking about it more, I prefer this approach, as it doesn't modify any existing language concepts (like var) and provides a nice shorthand, compared even to current declarations with var.

@rchoi1712
Copy link
rchoi1712 commented Jul 26, 2023

Also:

  • w.r.t. val, let, and var, val has the advantage over let in that it's more appropriate for uninitialized declarations (e.g., instance variables and lazy initialized locals). [Removed] fin could work, but is less ideal imo due to it being ambiguous - could mean finally, finalizer, etc., and also is phonetically quite distinct from final.
  • := is used in Go [Removed], and can be used for untyped initialization when final-by-default is turned on.
  • mut from Rust can be used to indicate mutability*** for function parameters, de-structured pattern assignments, and typed declarations, when either final-by-default gets turned on for specific scopes (e.g., packages), or globally.

[Removed ]


** More analytically, one could argue the compiler is the true audience for the var, val and final keywords. Devs really just want to write-and-forget, and have the compiler spit out error messages when code tries to do the 'wrong' thing, i.e., write to an immutable, or declare a mutable variable in a deeply immutable class.

*** I believe mut is better than var for this task, because the term 'variable' in programming actually has no connotation of mutability, and to the extent var means both 'variable' and 'mutable', it is a conflated term that - not surprisingly - seems to be causing some confusion.

@ds84182
Copy link
ds84182 commented Jul 26, 2023

Not a fan of :. Not very searchable, and variable shadowing across statement blocks would have to become an error to avoid very obvious footguns.

val is better suited for this. let may conflict with let expressions.

@rchoi1712
Copy link
rchoi1712 commented Aug 26, 2023

val is better suited for this. let may conflict with let expressions.

Let expressions - as defined by the team, are great, and will help in the move away from final.

If the team thinks val (as proposed in #3298) is worth experimenting with, I have these CL's ready for review:

https://dart-review.googlesource.com/c/sdk/+/322841
https://dart-review.googlesource.com/c/sdk/+/322842

@johnpyp
Copy link
johnpyp commented Apr 25, 2024

I see those two CLs are currently abandoned. What are the next steps for this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests