Open lrhn opened 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:
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 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.
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.
Note that let
is a non-final binding in JavaScript so it can be confusing for people coming from JS.
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.
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.
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
Swift also uses let
and has the same meaning as final
in Dart.
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.
It's about the binding of the new declared name to an object. That object may be mutable. For immutability of instances, check #125.
@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
Right, Swift let
has a rhs effect for struct
but not for class
. So it's moot given Dart doesn't have structs.
let result = when { }
is aesthetic for expression/functional style programming.
Hold on, can Dart do that? :thinking:
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.
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
)
@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.
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:
Most local variables are not re-assigned.
The author of the code wants to record their intent that the variable cannot be reassigned.
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:
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.
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.
+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.
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).
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. :-/
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.
Those seem like pretty compelling arguments, I'm sold.
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.
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.
And in GitHub. It also doesn't cause a warning if you assign to a variable that were declared final.
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.
It's fine adding that but if possible not at the expense of other helpful features like final
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.
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.
@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.
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.
let
could be preferable toval
or:=
for immutable bindings:
val
implies the rhs is a value object Kotlin style, not just a binding.val
is tricky to distinguish fromvar
when reading/eyeballing code.let
is used in kernel.let
is more pleasant to touch-type on english keyboards thanval
.:=
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
+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.
If final
is set to default, the program will become confused, easier to read if using let
@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? :)
In scala we use var
and val
with no issues 😁
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.
What if we rename final
to fin
? Or is that a keyword?
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 😁
So like lateinit var
? Not surprised, people often criticize it in Kotlin too.
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 😂
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>()
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.
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 ;)
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
I actually disagree that final
is too long at this point.
const
final
var
const and final are grouped visually by the character length
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.
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:
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
withval
. 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):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.