language icon indicating copy to clipboard operation
language copied to clipboard

The `final` keyword is too long

Open lrhn opened this issue 6 years ago • 88 comments

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 avatar Dec 17 '18 09:12 lrhn

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:

  • http://dart-lang.github.io/linter/lints/prefer_final_locals.html
  • http://dart-lang.github.io/linter/lints/omit_local_variable_types.html
  • http://dart-lang.github.io/linter/lints/parameter_assignments.html

pschiffmann avatar Dec 17 '18 10:12 pschiffmann

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.

peldritch avatar Dec 17 '18 15:12 peldritch

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.

zoechi avatar Dec 17 '18 16:12 zoechi

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.

yjbanov avatar Dec 17 '18 18:12 yjbanov

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

mraleph avatar Dec 17 '18 21:12 mraleph

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 avatar Dec 18 '18 03:12 peldritch

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.

peldritch avatar Dec 18 '18 03:12 peldritch

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

Hixie avatar Dec 18 '18 06:12 Hixie

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

kasperpeulen avatar Dec 18 '18 13:12 kasperpeulen

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.

peldritch avatar Dec 18 '18 14:12 peldritch

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

eernstg avatar Dec 18 '18 15:12 eernstg

@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.

yjbanov avatar Dec 18 '18 21:12 yjbanov

@yjbanov

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

peldritch avatar Dec 18 '18 22:12 peldritch

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

Hold on, can Dart do that? :thinking:

Zhuinden avatar Dec 31 '18 22:12 Zhuinden

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.

dlepex avatar Jan 02 '19 21:01 dlepex

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)

Zhuinden avatar Jan 02 '19 22:01 Zhuinden

@dlepex

I like final because it stands out.

That depends on how much change is possible.

I'd prefer https://github.com/dart-lang/language/issues/136#issuecomment-447940385 any time.

especially if you cannot justify them.

I think I did.

zoechi avatar Jan 03 '19 06:01 zoechi

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.

munificent avatar Jan 03 '19 21:01 munificent

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.

Hixie avatar Jan 04 '19 00:01 Hixie

+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.

natebosch avatar Jan 04 '19 00:01 natebosch

FYI, just filed related https://github.com/dart-lang/language/issues/160 (if immutable shared objects also support sharing closures then the ergonomics of final will be even more important).

yjbanov avatar Jan 04 '19 01:01 yjbanov

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. :-/

munificent avatar Jan 04 '19 21:01 munificent

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.

Hixie avatar Jan 04 '19 21:01 Hixie

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.

munificent avatar Jan 04 '19 21:01 munificent

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

Hixie avatar Jan 04 '19 22:01 Hixie

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.

davidmorgan avatar Jan 07 '19 13:01 davidmorgan

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.

Hixie avatar Jan 07 '19 15:01 Hixie

And in GitHub. It also doesn't cause a warning if you assign to a variable that were declared final.

zoechi avatar Jan 07 '19 16:01 zoechi

Right. But people use var or final without type annotations, and people use unqualified imports instead of show, which is the same trade-off: it makes something visible only using the analyzer, not directly in the source. So it's not too shocking to lean on the analyzer.

davidmorgan avatar Jan 08 '19 14:01 davidmorgan

It's fine adding that but if possible not at the expense of other helpful features like final

zoechi avatar Jan 08 '19 15:01 zoechi