-
Notifications
You must be signed in to change notification settings - Fork 213
Interpolation is letting a null string bypass null safety feature #2053
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
Comments
The value that is assigned to You might object to the fact that a nullable value can be interpolated, instead of requiring it to be non-null, but when we discussed this it was universally felt that this would be user-unfriendly on balance. It's quite painful not to be able to print out nullable values without conditionally checking whether they are null or not and printing out different things in the two cases. |
I understand your comment about the type system here and there are two things I feel about this:
I would recommend
|
The pitfall here, IMO, interpolation is letting the null string bypass the null safety feature. Consider this example: Let's say we have a data variable called Text('the last updated time is ' + lastUpdatedTime) Text('the last updated time is $lastUpdatedTime') In the first scenario, the compiler will complain about type system, which in turn you might add a condition to see if it isn't null(or add a !) and then show the Text widget but in the second case, you have a situation you might end up with "the last updated time is null". That is the pitfall and my concern here. But saying that, the core reason is, null has a toString method which spits out "null". So even if you use option 1 with int, you will end up with "null" string. I personally am not going to use interpolation anymore in my production code because I often use nullable strings. Anyway, Thank you for the reply, null! ;) Appreciate the explanation about how interpolation works. |
Renaming issue |
Here is a quick example that demonstrates the issue more clearly with Flutter UI. The idea is the data takes 2 seconds to load. Without interpolation the experience is much better because null safety kicks in and you end up showing a better UX instead of "null" strings. |
I wouldn't be opposed to a lint rule that warns about a nullable-typed expression in a string interpolation. |
If there was a lint rule there needs to be an alternative to print potentially null value (there is the + apparently but you lose interpolation). This is such a common use case, every class that has nullable members and a |
What do you mean, "an alternative?" An alternative lint rule? |
If there would be an alternative to interpolation where you would be forced to use bangs on nullable values, that would be a great solution. Appending with + also suffers from "null" string for types other than String because you would be forced to pull out the |
What are the consequences of removing |
@MarsGoatz |
Null has |
@AlexanderFarkas Please go through the whole thread for context. Also have a look at this example https://dartpad.dev/f2093518fb25acb7e5de6908174e6d2e |
You wouldn't be able to call In any case, I can see where I want to explicitly control what text is used in lieu of a null value. I would use a lint rule that warned about |
Thanks @srawlins! Wouldn't w.r.t the lint rule, I am with @tatumizer if it can get annoying, specially with false positives. |
I can imagine - "Hey User, You bank balance for today is null" 😆 |
Hey @leafpetersen, I have more context now, thanks to replies from you, @srawlins and @tatumizer. So here is the summary. If you haven't read already, I would recommend you go through:
Here is my take on this: With dart introducing null safety, you get used a certain paradigm, i.e, you simply can't assign a right hand expression which contains a nullable variable to a left hand variable which is non nullable. IMO, this is very central to what has been introduced as part of null safety. So both interpolation and appending with The root cause is I am by no means a dart expert but here are my recommendations:
I feel linting might not be the solution here if it was possible, only a duct tape. |
I would go for something that explicitly suggests that nullable objects aren't allowed and make it agnostic of business logic, e.g., user strings or server side strings. In the end whatever we chose should nicely blend in with existing dart ecosystem and we don't want devs to feel "oh this doesn't feel like dart". |
This is pretty cool! Thanks for the share, learning something new everyday. I would go with But TBH, can't help but feel this is contradictory to what dart should provide by default. In an ideal dart language we should be using If Flutter delivers on its promises, in five years, we will looking at tons of Dart users. I would honestly take the opportunity to make the right fix now and bring interpolation and appending with |
This is an interesting example (thanks for the dartpad, that was very helpful!) All of the suggestions above basically boil down to having (1) a compile-time check to ensure that you (2) insert a run-time check at each use, and perhaps when doing this also (3) realize that the One option not mentioned yet is to use Explicitly modelling the pre-valid → valid transition, together with class _InterpolationExampleState extends State<InterpolationExample> {
late String name;
late String motivationalQuote;
bool _ready = false;
@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 2), () {
setState(() {
name = 'Awesome Dev';
motivationalQuote = 'Flutter is awesome!';
_ready = true;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Interpolation'),
),
body: Center(
child: _ready
? Text('Hey $name. Your motivational quote is $motivationalQuote')
: CircularProgressIndicator()),
);
}
} |
Representing nullable value as non nullable using IMO, You use |
I get what you are saying, we all want some form of truth in the language. That is the language is logically correct and finds bugs at compile time every time, everywhere. But it just seems that for every instance where you wouldn't want interpolation for null values you'd have a dozen instance where you would want it. Meaning that this is an edge case. I really wish Djikistra dream was doable but in practice it seems impractical so we end up with things that are somewhat correct. At the end of the day, tests will tell you if your program works correctly, not whether the code compiles or not. |
Quite the opposite 😉. Let us consider a modification where the program allows certain features without logging in. This is represented by the You can increase the chances of finding use-before-ready errors several ways. One is to use String? _name;
String? _motivationalQuote;
bool ready = _name != null && _motivationalQuote != null;
String get name => _name!;
void set name(String value) { _name = value; }
String get motivationalQuote => _motivationalQuote!;
void set motivationalQuote(String value) { _motivationalQuote = value; } This basically amounts to an ad-hoc implementation of |
@tatumizer I think I agree with you about I am hoping Dart team can come up with tagged word for forcing devs to handle null values in interpolation, maybe |
@rakudrama I understand the point you are making and I appreciate the examples. So, as a Dart user via Flutter, with null safety feature, I am thinking that Dart forces you to handle your null values. I understand that interpolation isn't really null and string "null". In fact, this is more dangerous as I think of it as a silent failure. I am no Dart expert by any means but If I have to handle null values in my logic anyway, i.e., map nullable value to a non null bool value to see if the value is ready, what was the point of null safety? I would have done that pre null safety too. e.g. With null safetyString? name;
//in Flutter UI, because dart will complain, null safety kicks in
name != null? Text('Your name is ' + name) : ShowSpinner();
//with interpolation you could end up with
Text('Your name is $name'); Your example suggests: String? name;
bool isNameReady => name != null;
//in Flutter UI
isNameReady? Text('Your name is $name') : ShowSpinner(); Without null safetyString name;
bool isNameReady => name != null;
//in Flutter UI
isNameReady? Text('Your name is $name') : ShowSpinner(); |
I think as an end user of Dart, I look at different things and as a bare bones Dart developers you and others look at different things. There was definitely a gap and as you and others asked me the right questions and we exchanged information, we bridged the gap. I would call that a success.
Thank you for providing me the bigger picture here! I think I understand where the issue stems from.
Couldn't have said it better myself, you hit bulls eye here. Cheers! 👍🏾 |
I think the underlying conflict here is (1) For instance, in an application context it is quite likely that it is always a bug if an interpolated string ends up containing So should this have been the other way around? We'd probably have to provide the most low-level operation that anyone could ever need in any case, and then any "higher level" features would have to be built on top of that. For instance, how about creation of strings that are safe in some sense? Maybe they can't be more than 80 chars long, maybe they can't contain Unicode code points that give rise to a switch in writing direction, maybe they are not allowed to contain database commands that could enable a black hat attack, etc.etc. I tend to think that in the situation where a The null object wouldn't have the method This kind of check might need to be rather expressive, because we might want to ensure that one of many "appropriate" methods is used, not just a single specific method, and it would need to be under developer control, such that we can all choose exactly the name we want to use for that "application domain toString" thingy, and check it in exactly the manner which is appropriate for the given application domain. Perhaps we could delegate that task to enhanced string literals? Cf. tagged strings. Perhaps |
I've put this on the agenda for the team to talk about at one of our next meetings. |
Everything here assumes a string. int? age;
var str = "Age: $age";
// vs
var str2 = "Age: " + age.toString(); // toString call is needed. then the The main issue here is that some functions and language features accept any object. FutureOr<String> name = Future.value("yup");
// mistaken logic here allows you to flow through
var s = "Name is: $name"; then it won't complain that The one issue that string interpolations have is that the underlying |
The original issue discussing this is dart-lang/sdk#57271. I agree that it's very annoying to have I can't imagine us taking |
@tatumizer how is this issue related to null ? using toString on null might be a bug: int a = 3;
int? b = 4;
Text('a: $a, b: $b'); Currently we can do this: int a = 3;
Future<int> b = Future.value(4);
Text('a: $a, b: $b'); Which is also an user made bug for client facing strings. That's why future was mentioned. It can be extended to any kind of value that's not a number or String. This is a bug too (most likely): int a = 3;
Result b = Result(4);
Text('a: $a, b: $b'); I'm failing to see how those 3 cases are different. So I gather maybe what you want is an operator that does not call |
Why would you have to do that? I think |
I believe that comment was regarding making |
We discussed this in the meeting this morning. Some notes:
@pq are you interested in evaluating this as a lint candidate? Part of that evaluation could include doing some data gathering that might feed into a language decision on this in the future. |
I started a lint proposal in dart-lang/sdk#58615. Feel free to add details or concerns. I agree that collecting some real-world data would be really useful here. |
I would really like to see |
We'd probably want both We also have to specify what "Hello $user, your report is $?userReport" is a good example, because a result of The one thing that using an empty string would hurt is debug-prints, where you might want the The default should be the one that is correct in most cases where you have a nullable value and want to put it into a string. (I guess research is required!) |
I like the idea of using the tagged strings proposal for something like this. Rather than just using a "nonnull" tag processor specifically to filter out nullable expressions, it could be broadened to a processor that outputs a "user-facing string" which could combine the null safety with things like i18n support (through a keyless implementation similar to this). Methods responsible for presenting text to the user could be altered or given variants to take in these user-facing strings, encouraging their use where they're appropriate and leaving everything the same where they aren't. |
Maybe this is a point to consider. Often when I write a dataclass-like class, I will define a void main() {
final person = Person(firstName: 'Matthew', lastName: 'McDonald');
print(person);
}
class Person {
final String firstName;
final String? middleName;
final String lastName;
const Person({
required this.firstName,
this.middleName,
required this.lastName,
});
@override
String toString() =>
'Person(firstName: $firstName, middleName: $middleName, lastName: $lastName)';
} The above prints out:
and seeing "null" in the output is the intended behavior here. |
Personally I don't think this operator would add value compared to the alternatives. Today you can write:
Such operator is only useful for edge cases for a fraction of people, while helper functions, be it tagged strings or just a function, gives the user the flexibility his project requires. I just don't think this is a decision that should be built-in the language . I'm also against removing |
@cedvdb, I agree that there shouldn't be a decision made as to what the language is ( // Common use-case 1: print a string with no nulls in it:
final nonNull1 = "Value is: $value"; // new
final nonNull2 = "Value is: ${value!}"; // current
// Common use-case 2: print a string with 'null' in it:
final nullable1 = "Value is: $?value"; // new
final nullable2 = "Value is: ${value ?? 'null'}"; // current Note that this doesn't involve removing |
Why specifically null values though ? Why isn't Future or any other instance not a problem that also need safe guarding ? Is it because it is less frequent ? If so the problem will remain for those other cases. void main() {
final a = Future.value(3);
// this could be a user facing string
print('value of a: $a');
} I find it would be better to have an operator that only works with void main() {
final a = Future.value(3);
// compilation error
print('value of a: #a');
// accepted
print('future: #{a.toString()}');
} Here you gotta go out of your way to show a "dev string". Be it null, Future, or a class instance. |
We have, for better or worse, made On the other hand, string interpolation is inherently a low-level and permissive operation. It allows any object, because every object has a The way to make a type-safe interpolation is to have a helper function We have more functions which accept As might be clear, I'm very unclear about what would be the best thing to do here, or whether doing anything is worth it. |
What about
This kills two birds with one stone. I don't find the argument that null is a special value relevant in this instance, I also find removing It can be accomplished with tagged strings but language supports might make sens since dart is advertised as a client language.
I for one like null safety but focusing on null here is imo a mistake. It is not an instance where null is a problem it's any value that is not supposed to be used in UI and when not using internationalization that's probably any value beside strings and numbers. With internationalization it's a non issue since interpolation isn't used. |
I don't think they forget that. That's why they happily use types other than
Why numbers, then? The var x = 0.1 + 0.2;
print("$x"); to print var billion = 1000000000;
var bignum = billion * billion * billion;
print("$bignum"); to print a one followed by a lots of zeros ... you'll be surprised again (it's "1e+27"). If anything, I'd reject doubles too. They are at least as dangerous as a So, strings and integers, and then we might as well just say strings only. And, suddenly, interpolations stops being that much more convenient than just using Arguably, you shouldn't use strings at all when creating user-facing text. Every text should be templated, internationalized and every template parameter should be formatted deliberately (preferably in a locale specific way), after passing security validation. The "perfect" is a very high goal. I don't think we're aiming for that. The question is if there are low-hanging fruits that can catch some easy-to-make mistakes, without getting in the way of interpolations actually being convenient. |
The thing is that it would not change anything about current interpolation. You'd have to use a special token
Fair enough, it's a arguably a nicer syntax though.
Agreed, that is principally why I was against "catching the low hanging fruits" since to me this is a non issue and you are going to impact people for which it is a non issue by throwing the low hanging fruits at them. I hope it won't change how it is today, that is I hope null will keep working in interpolation as it does today. If something needs to be done I suggest using an alternative token like I guess I've made my point so I'll unsub from here to not be too dense. |
Without debating about the other aspects of this issue, I do think that's a quite important feature. It would bring a significant improvement for code-generators, who use string templates / interpolation quite a lot. |
I think it's significant what lrhn pointed out. That's kind of how python works: >>> a = None
>>> print(f"jkl{a}")
jklNone
>>> print("jkl" + a)
Traceback (most recent call last):
File "<pyshell#11>", line 1, in <module>
print("jkl" + a)
TypeError: can only concatenate str (not "NoneType") to str
>>> print("jkl" + str(a))
jklNone This gives a safer way to make UI strings (use + instead of interpolation). |
The issue is not "forgetting" more so than no future changes compilation error, which is kind of the point of type safety. While I object the premise of null being the issue here, I still think that a stronger string interpolation would be helpful:
Which is later changed to:
A new syntax like with # that would require either a String ,Numbers or boolean would be nice
Admittedly as soon as toString() is called on a variable the same issue as mentioned above can be back, but at least it protect one level... As writing this I'm not even sure this is a good idea. |
There is no type safety issue because there is no type issue. You can call That's not particular to interpolations, it applies to any operation which accepts any object. In the example MyClass value = myClass.value;
return 'value is ${value}'; then you'll get a warning when the type of Object? value = myClass.value;
return 'value is ${value}'; you wouldn't get any warnings, and you wouldn't expect any. return 'value is #{myClass.value.toString())}'; then you are back to square one, because you can call You can't fix a type problem using an inherently untyped feature. The moment an interpolation, or you, or anyone, calls We could perhaps remove The alternative, to make Or, most likely, we can introduce a lint warning about nullable expressions in interpolations. Then you can fix them if you care, and don't have to if you don't. |
I think you are speaking in general terms, but where this issue is most likely to be a problem is in UI, or some output text. I start with the premise that the values that are going to be rendered in UI are either String, the name of a person, or a number, his age; Boolean don't even fall there. The rest is the role of localization. If that assumption is true The argument is that Note:
|
So you are asking for a new feature, which is used to create strings "intended to be rendered in the UI". That feature is like string interpolation, except that the expressions can only be strings or numbers. (I'd say integers, doubles are right out!) So, maybe just strings. That's very specialized. Not all strings are going to be rendered, which is not a problem since we'll still have the normal string interpolation to create the rest. It's also not particularly user friendly. If you have an object, and want a string representation of it, you can do "#{foo.toRenderString()} ..." but you can also do that today with If your object actually uses its What you should be doing instead is to write proper macro functions: String renderFoo(Foo foo) => "value is: {$foo.value}"; // Double checked and tested! Again, you can do this today, you don't need a different string interpolation for that, one which doesn't actually solve all the problems. All in all, I don't think a restricted interpolation is actually the best way to solve the problem. Rejecting anything non-string in an interpolation is just a weaker feature than normal interpolation, but not a much safer one. It's possible, but, IMO, not worth the necessary effort (new syntax costs a lot of developer resources). |
I tried this on dartpad this prints
null
.Feel like this is a hack and shouldn't be allowed? So submitting an issue.
EDIT:
Please go through this example for complete context https://dartpad.dev/f2093518fb25acb7e5de6908174e6d2e
Also, this reply : #2053 (comment)
EDIT 2:
Summary: #2053 (comment)
The text was updated successfully, but these errors were encountered: