dart-lang / language

Design of the Dart language
Other
2.67k stars 205 forks source link

The `final` keyword is too long #136

Open lrhn opened 5 years ago

lrhn commented 5 years ago

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.

pschiffmann commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

peldritch commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

peldritch commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

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

Hold on, can Dart do that? :thinking:

dlepex commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

munificent commented 5 years ago

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:

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

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 commented 5 years ago

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 commented 5 years ago

+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 commented 5 years ago

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

munificent commented 5 years ago

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.

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

davidmorgan commented 5 years ago

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 commented 5 years ago

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.

zoechi commented 5 years ago

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

davidmorgan commented 5 years ago

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.

zoechi commented 5 years ago

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

dlepex commented 5 years ago

VSCode has a pretty nice plugin: https://github.com/siegebell/vsc-prettify-symbols-mode

It can reduce any obnoxiously long keyword/operator into something really short. For instance "final" into "val", or even more radically to something like "◇", which is already quite close to Python/Go.

MarcoLeongDev commented 5 years ago

let and var does not introduce noise themselves:

var a = 1;
let b = 2;
var c = 3;

foobar(){
   print(a);
}

final as a keyword is visually noisy:

var a = 1;
final b = 2;
var c = 3;

foobar(){
   print(a);
}

This concept is mentioned by Kevlin Henney in ITT 2016, focusing on coding practices: Code should be written so that visually it is understandable. A good indicator would be that the brain should not be struggling to classify the code into logical block with a glance at a page of code. Shown by the code above.

Base on this concept, final as a keyword is considered as bad design because the language is introducing the noise by default.

Going further into the discussion: Your brain is mapping var and let as one block. If you require more information by focusing on the declaration "visual block", var and let starts and ends with different letters to given enough distinction when compare to the choice of var and val.

p.s. IDE/plugin should not be counted because they are not available when people review codes on their tablets. It also doesn't work with video and keynote slides as mentioned by Hixie in his/her comment.

yjbanov commented 5 years ago

@tatumizer I think before we start thinking of a specific syntax we should figure out the right immutability semantics we want. There are at least 3 popular use-cases that we need to decide on: shallow immutability (see e.g. https://github.com/dart-lang/language/issues/125#issuecomment-457132947 by @eernstg), non-shared deep immutability, x-isolate memory-shared immutability (@leafpetersen's proposal). It is not clear yet whether immutability will be a property of the type, a runtime property (I could swear I saw a proposal fly by, but can't find it anymore), or something modifier-driven. For the latter case your ^ could be used as a modifier.

richard-uk1 commented 5 years ago

Just to show prior art in other languages:

Rust uses let for immutable, let mut for mutable, and const for compile-time constant.

You probably don't need the let in class variables, so you could have


class MyClass {
    String immutableString; // this is equivalent to `final String immutableString` today.
    mut String mutableString; // This is equivalent to `String mutableString` today.
}

Here you're swapping the onus - things are immutable by default and you have to request mutability if you need it.

minh98 commented 5 years ago

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.

i think i can typing 'val' faster than 'let' :D

rrousselGit commented 5 years ago

+1 for a setting to have final as default (a field in the pubspec.yaml?)

This is especially interesting for functions parameters.

I've realized some time ago that we could do:

void foo(final int bar) {
  bar = 42; // compile error
}

But specifying final for all parameters requires far too much boilerplate.

minh98 commented 5 years ago

If final is set to default, the program will become confused, easier to read if using let

sethladd commented 5 years ago

@inmatrix and I did some user studies almost two years ago (??) about this topic and we had some interesting insights. Maybe @inmatrix can dig up the notes? :)

psyanite commented 5 years ago

In scala we use var and val with no issues 😁

Zhuinden commented 5 years ago

another outlandish (in a good sense!) idea.

\\ for immutable

\ for mutable

I'm a bit confused, it still takes extra effort to make something immutable than mutable, and the clarity of var and let/val is lost in comparison. Now you have to count \s to know if something is mutable or not. Seems hard to read to me.

psyanite commented 5 years ago

What if we rename final to fin? Or is that a keyword?

erf commented 4 years ago

I like let. let and var would be consistent with the Swift language - which is awesome.

Now just add pattern matching, enum with values, tuples and guards 😁

Zhuinden commented 4 years ago

So like lateinit var? Not surprised, people often criticize it in Kotlin too.

psyanite commented 4 years ago

In Scala we use lazy val customers = customerSvc.getAllCustomers() We have var (reassignable variable) and val (non-reassignable value).

I have to context switch between Scala, Typescript and Dart 😂

lukepighetti commented 4 years ago

What about allowing people to use final or fin? I personally like final, my only reason for using var these days is that when I'm switching between a TypeScript and a Dart codebase if I use var all the time it requires less context switching.

I do not agree that assignments that start with a type should be a final. The reason is usually final and var can infer the type, but if you're building out a state class that starts with a null value like String _lazyFoo; that scenario will 100% of the time be mutable var. Would it be too weird to make it var if nothing is assigned and final if it is? ie String _lazyFoo == var String _lazyFoo and String _lazyFoo = "" == final String _lazyFoo == ""? I'm not a huge fan but it's an idea.

I am not in support of removing final because it's nice to do

final _subject = BehaviorSubject<MyAuthenticationModel>()

instead of being forced to do

BehaviorSubject<MyAuthenticationModel> _subject = BehaviorSubject<MyAuthenticationModel>()

willhaslett commented 3 years ago

I'm confused by the discussions above linking final to the concept of mutability. I'm relatively new to Dart, but doesn't final simply mean that the declared variable cannot later be assigned to a different object, not that the object to which it refers cannot be mutated? For classes with no mutable public properties, like String, int, etc., this may be effectively the same thing, but this is mutation, where Frog's default constructor exposes a color property:

final Frog frog = Frog();
frog.color = 'pink';

I mention this because it seems important not to confuse what final does with immutability. That exact confusion cost me some growing pains when getting started with the language, and it's a reason I'm not a fan of final, in addition to it taking up space.

davidmorgan commented 3 years ago

Correct. After much discussion many of us have settled on always using var for locals; it's why I added the lint unnecessary_final.

Deep immutability is as you point out a very different thing. I like to blog about why it's important ;) and have provided built_collection and built_value for your deep immutability needs ;) I also hear freezed is popular ;)

lukepighetti commented 3 years ago

What's super interesting is people who write Flutter tend to use final to communicate that which should never change regardless of immutability, while people who write in other Dart projects tend to use var for locals regardless of if it should change or not. Why this divergence has occurred is not clear to me

lukepighetti commented 3 years ago

I actually disagree that final is too long at this point.

const
final
var

const and final are grouped visually by the character length

munificent commented 3 years ago

Why this divergence has occurred is not clear to me

I think a lot of Flutter users adopt the style of the Flutter framework itself, which is quite different from the prior idiomatic Dart style.