Why an explicit ".cast<>()" function in Dart instead of "as <>" - dart

In my question Dart 2.X List.cast() does not compose the answer requires converting a List<dynamic> to a List<String> as such:
List<String> ls = (json['data'] as List).cast<String>().map((s) => s.toUpperCase()).toList();
My experience from other languages had me write this first:
List<String> ls = (json['data'] as List<String>).map((s) => s.toUpperCase()).toList();
Note that this compiles but fails at runtime in Dart 2.
Why does Dart typecasting for a List require a function as List).cast<String>() as opposed to simply using the Dart as "typecast operator" such as as List<String>?
---- Edit ----
I am using the most recent Dart 2.0.0-dev.43.0 and get inconsistent runtime behavior with as typecasts/assertions. Isn't the .cast<>() function creating a new iterable the same as a .map()? Changing my code to this works:
List<String> ls = (json['data'] as List).map((s) => (s as String).toUpperCase()).toList();
This seems to take advantage that the first cast to List is a List<dynamic>. Thus the .map function parameter is also a dynamic.
My second example above with to as List<String> works in some places in our code but not others. Note that IntelliJ correctly infers the types in all of the above examples - it's the runtime where the failure happens. I'm guessing that the inconsistent behavior is due to Dart 2.x being still in development.
---- 2nd Edit ----
Here are my test cases that I have in one of my class constructors:
Map<String, dynamic> json = { "data": ["a", "b", "c"] };
//List<String> origBroken = json["data"].map( (s) => s.toUpperCase() ).toList();
// Sometimes works - sometimes gives "Ignoring cast fail from JSArray to List<String>" at runtime!!!
List<String> wonky = (json["data"] as List<String>).map( (s) => s.toUpperCase() ).toList();
print("Wonky $wonky");
List<String> fix1 = (json["data"] as List).cast<String>().map( (s) => s.toUpperCase() ).toList();
List<String> fix2 = (json["data"] as List).map( (s) => (s as String).toUpperCase() ).toList();
List<String> explicit2 = (json["data"] as List<dynamic>).map( (dynamic s) => (s as String).toUpperCase() ).toList();
// From accepted answer of the linked question - compile error because .cast() doesn't take parameters
// error: Too many positional arguments: 0 expected, but 1 found.
//List<String> notBroken = (json['data'] as List).cast<String>((s) => s.toUpperCase()).toList();
List<String> notBrokenFixed = (json['data'] as List<String>).cast<String>().map((String s) => s.toUpperCase()).toList();
The problem is the warning Ignoring cast fail from JSArray to List<String> sometimes given by the wonky assignment. When I say sometimes it's because it changes unpredictably as I make changes to the main application that uses the library that contains this code - without making changes to this class or even the library.
At the time I wrote the first edit above, wonky wasn't working. I just tried it again now and it's working. I have not changed any code in this library - I have been working in the main application which has a dependency on this code's library.
Some background, this is a multi-library project being converted from Angular/Typescript. These test cases are based on the processing we do to deserialize JSON into Dart classes. We map JSON (dynamic) strings into various data structures such as enums, Option<> and Either<> (from dartz) using class constructor initializers.
A couple weeks ago the runtime warning started happening I believe because of Breaking Change: --preview-dart-2 turned on by default.
I understand that this warning will soon be an error. So I traced the warning back to these conversions that map from JSON dynamic data (Yes, dynamic data is an edge case in Dart but it's what dart:convert provides).
We are developing on Mac using DDC with the most recent Dart 2.0.0-dev.43.0, angular 5.0.0-alpha+8, build_runner 0.8.0, IntelliJ 2018.1 and running on Chrome 65.0.3325.181.
---- Final Edit ----
There is an instability in the current development build/runtime that is behind this issue. No, I don't have a reproducible example. Changing and rebuilding our main app will cause this code in an unmodified library dependency to sometimes give the runtime warning Ignoring cast fail from JSArray to List<String>.
The suspect code from the original part of this question (also wonky above)
List<String> ls = (json['data'] as List<String>).map((s) => s.toUpperCase()).toList();
casts the dynamic JSON data to a List<String>. The types are fully constrained and the Dart analyzer/IntelliJ infers s to be Static type: String.
The runtime warning that sometimes occurs and related answers to use .cast() is what led to this question. At this time I'll believe the analyzer and ignore the runtime warning.

In Dart 2 generic types are reified.
as ... is more like an assertion, if the values type doesn't match as causes a runtime exception.
cast<T>() is a method introduced in Dart 2 on Iterable that actually creates a new iterable of type Iterable<T> (or in your case the subclass List<T>) filled with the values of the original interable.
Update
You can use print('wonky: ${wonky.runtimeType}'); to see what the actual type is.
If the type matches your requirement, you can use as to communicate it to the analyzer that it's safe to assume this type.
If the type doesn't match, for example because it is List instead of List<String>, then you can use .cast<String>() to actually make it a List<String>.
List<String> broken = (json['data'] as List)
.cast<String>((s) => s.toUpperCase()).toList();
Here you seem to try to use cast for casting and mapping, but that is not valid.
map() can do both though
List<String> notBroken = (json['data'] as List)
.map<String>((s) => s.toUpperCase()).toList();

Related

dart nullsaftey and using old packages

I've enabled the dart 2.8 nullsaftey experiment.
I've converted my app to nullsaftey but its using an old pre-nullsafety package.
The problem is that the old package has a method which can return null:
/// Returns the environment variable with [name] or null if it doesn't
/// exist
String env(String name);
Which is used as follows:
var home = env('HOME');
If the HOME environment variable is missing, env returns null.
The problem is that env is declared as returning a String.
So when I write
var home = env('HOME');
home ??= '/home';
I get an error:
The operand can't be null, so the condition is always false.
Try removing the condition, an enclosing condition, or the whole conditional statement.
Given that all the nullsaftey release announcements say you can use nullsaftey with older packages, I'm guessing there is some way to declare an imported packages as non-nullsafe.
Problem is that I can't find any documentation on how to do this.
null safety has not been released yet! that is why you need to provide the experiment flag.
Language versioning
By default, whether or not null safety is supported in a library is determined by its language version. Any language version 2.8 or lower counts as opted out of null safety, and 2.9 or higher (subject to change) is opted in. The language version itself can come from one of two places:
The minimum bound of the package's declared SDK constraint. The following package will have a language version of 2.8.
name: foo
env:
sdk:
">=2.8.0 <3.0.0"
A language override comment at the top level of the file, before any other declarations. The following library will have a language version of 2.8.
// #dart=2.8
class Foo {}
The language override comment will take precedence over the SDK constraint, but only within the single library where it is declared.
Interaction between null safe and non-null safe code
The problem you are having is reproducible without different packages or incorrect language versions though, and has to do with the interaction between null-safe and non-null-safe code. Consider the following example:
// #dart=2.8
String foo() {
return null;
}
// #dart=2.9
import 'a.dart';
void main() {
var value = foo();
value ??= 'asd';
}
The return type of foo doesn't become String?, instead it gets tagged as String* - this is known as a legacy type. A legacy type is treated as a non-null type in opted in libraries. The goal of legacy types is to make it easier to migrate to null-safety through an in-order migration
Consider the example below:
// #dart=2.9
void foo(String value) {
// do something with non-null String.
}
// #dart=2.8
import 'a.dart';
void main() {
foo(getStringFromAPI());
}
While foo requires a non-null string, it isn't possible for the entry-point to actually pass it one - since it has not opted in yet. Without the treatment of legacy types as non-nullable types, it would not be possible to gradually migrate - because all libraries would need to be updated at once, or only updated to accept nullable types.
Out of order migration
By calling code that has not been migrated to null-safety from a null safe library, you are increasing the risk that you will be broken when that dependency eventually migrates. In you example, if home was treated as non-nullable then updating to a version of the dependency with an updated return value of String? would cause a compilation error.
For your specific case, I would recommend specifically annotating the type of home as String?. This is a perfectly valid type annotation, since in general T and T* are always assignable to T?. It also more correct, since you know the API can return null.
String? home = env('HOME');
home ??= '/home';
EDIT June 2021:
Null safety has released, yay! The first version of Dart with null safety enabled by default ended up being 2.12 and not 2.9 as documented in the question above.

Type inference behaves differently for similar cases

Running the following code (Dart 2.3) throws the exception:
type 'List<dynamic>' is not a subtype of type 'List<bool>'
bar() => 0;
foo() => [bar()];
main() {
var l = [1, 2, 3];
l = foo();
}
However, this slightly altered example runs correctly:
main() {
bar() => 0;
var l = [1, 2, 3];
l = [bar()];
}
As does this:
main() {
bar() => 0;
foo() => [bar()];
var l = [1, 2, 3];
l = foo();
}
What is it about Dart's type inference algorithm that makes these cases behave differently? Seems like the types of the functions foo and bar should be pretty easy to infer, since they always return the same value. It also isn't obvious to me why moving around the site of the function declaration would change type inference in these cases.
Anyone know what's going on here?
Leaf Petersen explains it in a comment to dart-lang/sdk issue #33137: Type inference of function return value:
This is by design. We do infer return types of non-recursive local
functions (functions declared inside of the scope of another function
or method), but for top level functions and methods, we do not infer
return types (except via override inference). The reasons are as
follows:
Methods and top level functions are usually part of the API of a program, and it's valuable to be able to quickly read off the API of a
piece of code. Doing method body based return type inference means
that understanding the signature of the API requires reading through
the method body.
Methods and top level functions can be arbitrarily mutually recursive, which makes the inference problem much harder and more
expensive.
For primarily these reasons, we do not infer return types for top level functions and methods. Leaving off the return type is just another way of saying dynamic.
If you set
analyzer:
strong-mode:
implicit-dynamic: false
in your analysis_options.yaml file, then dartanalyzer will generate errors when top-level functions have an implicit dynamic return type:
error • Missing return type for 'bar' at example.dart:1:1 • strong_mode_implicit_dynamic_return
error • Missing return type for 'foo' at example.dart:2:1 • strong_mode_implicit_dynamic_return
It looks like nested functions are treated differently than top-level functions. It is probably a bug. I get the following from Dartpad on Dart 2.3.1.
foo() => 0;
bar() => [foo()];
main() {
baz() => 0;
qux() => [baz()];
print(foo.runtimeType);
print(bar.runtimeType);
print(baz.runtimeType);
print(qux.runtimeType);
}
// () => dynamic
// () => dynamic
// () => int
// () => List<int>
Explanation here:
This is expected behavior.
Local functions use type inference to deduce their return type, but top-level/class-level functions do not.
The primary reason for the distinction is that top-level and class level functions exist at the same level as type declarations. Solving cyclic dependencies between types and functions gets even harder if we have to also analyze function bodies at a time where we don't even know the signature of classes yet.
When top-level inference has completed, we do know the type hierarchies, and where top-level functions are unordered, they can refer to each other in arbitrary ways, local functions are linear and can only depend on global functions or prior local functions. That means that we can analyze the function body locally to find the return type, without needing to look at anything except the body itself, and things we have already analyzed.

Dart 2.X List.cast() does not compose

The upcoming Dart 2.X release requires strong typing. When working with JSON data we must now cast dynamic types to an appropriate Dart type (not a problem).
A related question Ignoring cast fail from JSArray to List<String> provides the answer to use the .cast<String>() function. Also a recent group messages says the same:
Breaking Change: --preview-dart-2 turned on by default.
The problem is that the .cast() function doesn't seem to compose. This original code when compiled using DDC and run in the Chrome browser:
Map<String, dynamic> json = { "data": ["a", "b", "c"] };
List<String> origBroken = json["data"].map( (s) => s.toUpperCase() ).toList();
Now receives the runtime warning (which will soon be an error)
Ignoring cast fail from JSArray to List<String>
So I add the .cast<String>() as the documentation and related link suggest and still receive the warning:
List<String> docFixBroken = json["data"].cast<String>().map( (s) => s.toUpperCase() ).toList();
List<String> alsoBroken = List.from( (json["data"] as List).cast<String>() ).map( (s) => s.toUpperCase() ).toList();
The code that doesn't give the warning requires a temporary variable (and also seems to be able to skip the explicit cast):
List<String> temp = json["data"];
List<String> works = temp.map( (s) => s.toUpperCase() ).toList();
So how can I write the cast and map as a single composed expression? The reason I need it as a single expression is that this expression is being used in an initializer list to set a final class variable.
I wrote Ignoring cast fail from JSArray to List<String>, so let me try and help here too!
So I add the .cast<String>() as the documentation and related link
suggest and still receive the warning:
List<String> docFixBroken = json["data"].cast<String>().map( (s) => s.toUpperCase() ).toList();
List<String> alsoBroken = List.from( (json["data"] as List).cast<String>() ).map( (s) => s.toUpperCase() ).toList();
Unfortunately, List.from does not persist type information, due to the lack of generic types for factory constructors (https://github.com/dart-lang/sdk/issues/26391). Until then, you should/could use .toList() instead:
(json['data'] as List).toList()
So, rewriting your examples:
List<String> docFixBroken = json["data"].cast<String>().map( (s) => s.toUpperCase() ).toList();
List<String> alsoBroken = List.from( (json["data"] as List).cast<String>() ).map( (s) => s.toUpperCase() ).toList();
Can be written as:
List<String> notBroken = (json['data'] as List).cast<String>().map((s) => s.toUpperCase()).toList();
Hope that helps!

In Dart2, what is the correct "anything" type to use for generics?

AngularDart has a class called AppView, i.e. abstract class AppView<T> {}.
One (at least) of these are generated for every class annotated with #Component:
// file.dart
#Component(...)
class DashboardComponent {}
// file.template.dart (Generated)
class ViewDashboardComponent extends AppView<DashboardComponent> {}
I have code elsewhere in the framework that doesn't care what this T type is. I'm a little confused with Dart 2 what the "right" "anything" type to use. For example, I could use:
AppView
AppView<dynamic>
AppView<Object>
AppView<Null>
AppView<void>
I think more than one of these will "work". But which is the "right" one to use in this case?
You should be fine to use AppView (or AppView<dynamic>) just about anywhere. I can think of two examples where this will get you into trouble though:
If you are instantiating an AppView, you definitely want that type parameter. See the following error when you don't:
$ cat a.dart
void main() {
List<dynamic> a = ["one", "two", "three"];
List<String> b = a;
}
$ dart --preview-dart-2 a.dart
Unhandled exception:
type 'List' is not a subtype of type 'List<String>' where
List is from dart:core
List is from dart:core
String is from dart:core
#0 main (file:///Users/sam/a.dart:3:20)
#1 _startIsolate.<anonymous closure> (dart:isolate/isolate_patch.dart:279:19)
#2 _RawReceivePortImpl._handleMessage (dart:isolate/isolate_patch.dart:165:12)
If you are ever assigning a closure to a site that expects a closure with one or more typed parameters that involve T, you will see a "uses dynamic as bottom" static error (from the analyzer), and probably a runtime error as well:
$ cat f.dart
void main() {
List a = <String>["one", "two", "three"];
a.map((String s) => s.toUpperCase());
List b = ["one", "two", "three"];
b.map((String s) => s.toUpperCase());
}
$ dart --preview-dart-2 f.dart
f.dart:3:9: Error: A value of type '(dart.core::String) → dart.core::String' can't be assigned to a variable of type '(dynamic) → dynamic'.
Try changing the type of the left hand side, or casting the right hand side to '(dynamic) → dynamic'.
a.map((String s) => s.toUpperCase());
^
f.dart:6:9: Error: A value of type '(dart.core::String) → dart.core::String' can't be assigned to a variable of type '(dynamic) → dynamic'.
Try changing the type of the left hand side, or casting the right hand side to '(dynamic) → dynamic'.
b.map((String s) => s.toUpperCase());
^
(I'm not certain any Dart tool yet has complete Dart 2 runtime and compile time semantics, so this might change slightly.)
In these cases, it is best to use generic classes, generic methods, and generic typedefs to encapsulate, for a given scope, what the values of an object's type parameters might be.
I suspect there is a difference between dynamic and Object in Dart 2, and I think Günter covered this in his response, though if your code "doesn't care what this T type is", then you're probably not calling any methods on the component.
Edit: void
AppView<void> might be a good choice in this case, as an actual check that you actually never touch the underlying component (Object would probably serve the same purpose). See how we are allowed to access properties of a List<void> but not properties of the elements:
$ cat g.dart
void main() {
var c = <String>["one", "two", "three"];
fn(c);
fn2(c);
}
int fn(List<void> list) => list.length;
int fn2(List<void> list) => list.first.length;
$ dart --preview-dart-2 g.dart
g.dart:9:40: Error: The getter 'length' isn't defined for the class 'void'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'length'.
int fn2(List<void> list) => list.first.length;
^
I assume you know better than me, but my attempt
AppView - works - same as AppView<dynamic>
AppView<dynamic> - works - really means any type
AppView<Object> - works - really means any type
AppView<Null> - won't work, only null and void values match for T
AppView<void> - won't work, only null and void values match for T
AppView<void> - works (see also comment below from lrn)
The difference between <dynamic> and <Object> would be that for values of type T with T == dynamic property or method access won't be checked statically, while for T == Object only methods and properties of the Object class can be accessed without a previous cast.

In dart web projects, shouldn't type and reference warnings be errors?

In dart, when developing a web application, if I invoke a method with a wrong number of arguments, the editor shows a warning message, the javascript compilation however runs successfully, and an error is only raised runtime. This is also the case for example if I refer and unexistent variable, or I pass a method argument of the wrong type.
I ask, if the editor already know that things won't work, why is the compilation successful? Why do we have types if they are not checked at compile time? I guess this behaviour has a reason, but I couldn't find it explained anywhere.
In Dart, many programming errors are warnings.
This is for two reasons.
The primary reason is that it allows you to run your program while you are developing it. If some of your code isn't complete yet, or it's only half refactored and still uses the old variable names, you can still test the other half. If you weren't allowed to run the program before it was perfect, that would not be possible.
The other reason is that warnings represent only static type checking, which doesn't know everything about your program, It might be that your program will work, it's just impossible for the analyser to determine.
Example:
class C {
int foo(int x) => x;
}
class D implements C {
num foo(num x, [num defaultValue]) => x == null ? defaultValue : x;
}
void bar(C c) => print(c.foo(4.1, 42)); // Static warning: wrong argument count, bad type.
main() { bar(new D()); } // Program runs fine.
If your program works, it shouldn't be stopped by a pedantic analyser that only knows half the truth. You should still look at the warnings, and consider whether there is something to worry about, but it is perfectly fine to decide that you actually know better than the compiler.
There is no compilation stage. What you see is warning based on type. For example:
This code will have warning:
void main() {
var foo = "";
foo.baz();
}
but this one won't:
void main() {
var foo;
foo.baz();
}
because code analyzer cant deduct the type of foo

Resources