One disadvantage of Dart's "primitives" is that while they map _better_ to JavaScript than say, GWT, they still require a non-trivial amount of global inference, wrapping/interceptors, and inefficient/defensive code in order to execute in JavaScript.
I totally get it for cross-platform libraries that are willing to sacrifice speed in order to be executable on both the VM and in the browser, but just like I imagine the VM will want to "tune" performance (for example, 64-bit integers), JS environments also want this feature.
This is a strawman for a "new" dart:js, based off the following
abstract class JsString {
external factory JsString(String from);
JsNum get length;
// ... And other members of the `String` prototype.
String toDartString();
}
abstract class JsNumber {
external factory JsNumber(num from);
JsBoolean isNaN();
// ... And other members of the `Number` prototype.
num toDartNum();
}
abstract class JsObject {
external factory JsObject(Map<String, dynamic> properties);
}
abstract class JsArray<T> {
external factory JsArray(List<T> elements);
}
abstract class JsMap<K, V> {
external factory JsMap(Map<K, V> items);
}
Using these should have zero cost over using direct JavaScript:
void main() {
var object = new JsObject({
'name': 'Matan',
'age': 90,
});
}
// compiles to
function main() {
var object = {
'name': 'Matan',
'age': 90,
};
}
Obviously it's great if dart2js can "infer" these from the primitive Dart types in many cases, but libraries that are very performance sensitive and _know_ they are only ever running in the browser can optimize internals:
void _doEfficientThing(Array<Person> persons) {
for (var i = 0, l = persons.length; i < l; i++) {
print(persons[i].name);
}
}
// compiles to (no interceptors, no range checks, etc)
function _doEfficientThing(persons) {
for (var i = 0, l = persons.length; i <l; i++) {
console.log(persons[i].name);
}
}
FYI, you can already create anonymous JS types which are represented with Objects in JS:
https://github.com/dart-lang/sdk/blob/master/pkg/js/lib/js.dart#L32
String/int/double should not really have overhead compared to JS, other than nullability (toDartString() and toDartNum() would be no-ops). We do not expose the native JS APIs on them, though. If these classes are interpreted as non-null, that could help a lot. If we had non-null types in Dart, then all Dart platforms would benefit.
I don't think JSArray can be generic, because there won't be any runtime type information to know what <T> is. Meaning we can't do casts or is checks. We'd also be skipping covariance checks, which is a huge soundness hole. We could solve those by having it be a non-reified, invariant generic, that you could never cast to/from any other type. But that'd be tough to use. (JSArray<T> in DDC and dart2js store the reified type, so they avoid the issues described above. But they also have Dart APIs, like []= that does a range check, a covariance check, and a growable check.)
JSMap<K, V> has the same problem w.r.t generics. Note that HashMap<K, V>.identity is a pretty thin wrapper over an ES6 Map (and some DDC optimizations are still on the way).
I just want to this that dart2js primitives performance is one of features where typescript wins. If it would be possible to directly instruct compiler about underlying js type, it would be possible to max out execution perf in browser where it is needed.
@jmesserly:
FYI, you can already create anonymous JS types which are represented with Objects in JS:
https://github.com/dart-lang/sdk/blob/master/pkg/js/lib/js.dart#L32
Yeah, it's not bad, but it is verbose to have to define a class, add annotations, etc, for some one-off, or for truly untyped classes objects. For example, I might want to store a string 鉃★笍 Array mapping, but not use Map (I don't need the overhead of hash checks, etc). A hypothetical JsObjectOf would be perfect here:
class Registry {
final _mapping = new JsObjectOf<Array>();
setMapping(String name, Array value) => _mapping[name] = value;
getMapping(String name) => _mapping[name];
}
... doesn't have to allocate a Map, doesn't have to call hashCode etc.
String/int/double should not really have overhead compared to JS, other than nullability (
toDartString()andtoDartNum()would be no-ops).
OK, so maybe dart:js should add a @notNull annotation to let me force that behavior 馃槃
We do not expose the native JS APIs on them, though.
That's unfortunate, because ES5+ has caught up quite a bit, and sometime has even _more_ functionality than the Dart APIs, and are pre-shipped in the browser. One example is Array#map versus List#map - the former is much better optimized by V8 (hacks, but still optimized).
If these classes are interpreted as non-null, that could help a lot. If we had non-null types in Dart, then all Dart platforms would benefit.
Is this entirely to be able to throw NoSuchMethod? If so I'd _kill_ for a --use-js-null-semantics, where _all_ objects are assumed to be non-null - and invoking object.foo() just throws a (JS) undefined instead of a NoSuchMethod.
I don't think JSArray can be generic, because there won't be any runtime type information to know what
<T>is.
That's fine with me, I don't want RTTI at all in Dart2JS if possible 馃懣
Meaning we can't do casts or
ischecks. We'd also be skipping covariance checks, which is a huge soundness hole.
I don't want covariance or soundness checks in production dart2js anyway. The is check is unfortunate, but I'd be happy with saying that is Array<Foo> is a compile-error, and instruct the user to use List if they want to do that.
We could solve those by having it be a non-reified, invariant generic, that you could never cast to/from any other type. But that'd be tough to use. (
JSArray<T>in DDC and dart2js store the reified type, so they avoid the issues described above. But they also have Dart APIs, like []= that does a range check, a covariance check, and a growable check.)
Yeah, I don't want any of these checks. I want to test my code instead.
JSMap<K, V>has the same problem w.r.t generics. Note thatHashMap<K, V>.identityis a pretty thin wrapper over an ES6 Map (and some DDC optimizations are still on the way).
Yeah, we are trying to use that more often in AngularDart, but it's a lot more typing. It also causes problems for our current Dartium users because Strings aren't identical in the VM, so we have to add silly code like:
Map<K, V> looseIdenticalMap<K, V>() {
if (identical(1, 1.0)) {
return new Map<K, V>.identity();
}
return new LinkedHashMap<K, V>(
equals(a, b): a is String ? a == b : identical(a, b),
...,
);
}
If these classes are interpreted as non-null, that could help a lot. If we had non-null types in Dart, then all Dart platforms would benefit.
Is this entirely to be able to throw
NoSuchMethod? If so I'd _kill_ for a--use-js-null-semantics, where _all_ objects are assumed to be non-null - and invokingobject.foo()just throws a (JS)undefinedinstead of aNoSuchMethod.
if we don't guard against null, JS does implicit coercions (null to 0, undefined to NaN)
It also causes problems for our current Dartium users because Strings aren't identical in the VM
ugh, yeah :\ ... but that problem should be going away soon, as no one will be using Dartium?
(FWIW, the Map perf change I'm working on will have Maps with String keys use an identity map under the hood, so you won't need extra typing)
I don't want covariance or soundness checks in production dart2js anyway.
[...]
Yeah, I don't want any of these checks. I want to test my code instead.
The problem when soundness is violated is the code can really go off the rails. A variable like T t; doesn't mean that t has a T anymore. The runtime value of t could be absolutely anything. And it's contagious, once you have a soundness hole, every type annotation in the program becomes meaningless.
Where this gets really bad is compiler optimizations. If the compiler assumes t is a T, but that assumption is not correct (because we disabled soundness checks), then the optimization becomes unsafe, and can change the program's behavior. The program can suddenly behave differently due to compiler implementation changes, without any changes in the application or library code. So in practice, lack of soundness limits the kinds of optimizations a compiler can do. While it seems like skipping checks is faster, it can end up worse overall.
(Personally I'd rather see generics in Dart be sound without runtime checks, similar to C# generics, but that's a different issue.)
(Also, this is kind of the fundamental difference between an approach like Dart and an approach like TypeScript. TypeScript can get away with unsound types because they're only used as a tooling aid, and are completely erased at runtime, and the JS code is directly executed rather than being optimized. We don't have that luxury for Dart types/code.)
@jmesserly:
If these classes are interpreted as non-null, that could help a lot. If we had non-null types in Dart, then all Dart platforms would benefit.
Is this entirely to be able to throw
NoSuchMethod? If so I'd _kill_ for a--use-js-null-semantics, where _all_ objects are assumed to be non-null - and invokingobject.foo()just throws a (JS)undefinedinstead of aNoSuchMethod.
if we don't guard against null, JS does implicit coercions (null to 0, undefined to NaN)
That's fine with me! That would be a warning under this flag - you are asking for JS-like semantics in production code - with the expectation you've well tested it in DDC and guard against bad behavior (similar to how JS or TypeScript users would have to).
It also causes problems for our current Dartium users because Strings aren't identical in the VM
ugh, yeah :\ ... but that problem should be going away soon, as no one will be using Dartium?
Yup. But that's just one example :)
(FWIW, the Map perf change I'm working on will have Maps with String keys use an identity map under the hood, so you won't need extra typing)
I saw, very exciting. In this particular case we actually don't know, it's a Map<Object, ...>, so we have to assume it could be anything, but we don't want to do hashCode and == checks for objects like protocol buffers.
I don't want covariance or soundness checks in production dart2js anyway.
[...]
Yeah, I don't want any of these checks. I want to test my code instead.
The problem when soundness is violated is the code can really go off the rails. A variable like
T t;doesn't mean thatthas aTanymore. The runtime value oftcould be absolutely anything. And it's contagious, once you have a soundness hole, every type annotation in the program becomes meaningless.
Again, fine with me under --trust-i-tested, or what ever flag name is UX-friendly. I'm not saying there shouldn't any checks ever - just that if I choose to exhaustively test my application in DDC (a combination of unit and e2e tests), and I trust it, I _don't_ want to throw any runtime soundness checks or NSMs (no-such-methods).
Where this gets really bad is compiler optimizations. If the compiler assumes
tis aT, but that assumption is not correct (because we disabled soundness checks), then the optimization becomes unsafe, and can change the program's behavior.
I'd only want users to use this flag _if_ they trust their application. I realize is T is something that might _need_ RTTI for execution:
void printLengthLike(dynamic item) {
if (item is List) {
print(item.length);
} else if (item is Lengthy) {
print(item.lengthOf());
}
}
In this case, I realize the compiler _needs_ to emit enough RTTI to be able to do those checks. I consider this mostly a hole in the language; for example if we had static overloads I wouldn't need this at all:
void printLengthLike(List item) => print(item.length);
void printLengthLike(Lengthy item) => print(item.lengthOf());
(Personally I'd rather see generics in Dart be sound without runtime checks, similar to C# generics, but that's a different issue.)
Interesting. What are the major differences? ELI5 馃惐
(Also, this is kind of the fundamental difference between an approach like Dart and an approach like TypeScript. TypeScript can get away with unsound types because they're only used as a tooling aid, and are completely erased at runtime, and the JS code is directly executed rather than being optimized. We don't have that luxury for Dart types/code.)
Why not though? DDC I think could be the defining difference between TS and Dart - it has both static and runtime checks so you can exhaustively unit test and e2e test your application, and once you are happy with the results you _could_ optionally compile with aggressive optimizations. We could even document that you should run your same suite of e2e tests with your new ".aggressive.dart.js" code because we don't guarantee 100% semantics.
I saw, very exciting. In this particular case we actually don't know, it's a Map
hmmm, I'd love to hear more. What I'm trying to do in my Map optimization CL is to avoid ==/hashCode for any object that doesn't override it. So even if you make a new Map it will still handle identity-equality objects pretty well. Seems to help a fair bit.
Of course new Map.identity is still a bit better if you can, because it saves an extra check for whether we have a custom ==. If you can use ES6 Map, then you should be able to use that one, I think?
In this case, I realize the compiler needs to emit enough RTTI to be able to do those checks. I consider this mostly a hole in the language; for example if we had static overloads I wouldn't need this at all [...]
Yeah that's a fair point. We're missing some features that are common in statically typed languages. As @leafpetersen often notes :), there are certainly languages without casts. Just a question of how usable that language would be, and what language or typing features we'd need for it.
(Personally I'd rather see generics in Dart be sound without runtime checks, similar to C# generics, but that's a different issue.)
Interesting. What are the major differences? ELI5 馃惐
Sure thing! Let's say I've got a Iterable<int>. It will produce a sequence of integers: 1, 2, 3. I could safely cast that to an Iterable<num> or Iterable<Object>, because all of those values are numbers and objects. So Iterable<T> is always safe to use covariantly, without runtime checks. Similarly if we had an interface ReadOnlyList<T>, which was like List<T> but only the APIs for reading from it, that would be safe to cast to ReadOnlyList<Object> for any T.
There are also safe-contravariant interfaces, like Comparable<T>. If I can compare any Objects, then I can safely compare two integers or two strings. A Set class where I could access APIs like add contains remove but not lookup or anything that returns the items would also be safely contravariant.
Interfaces like List<T> are invariant, it is not safe to cast them to another T. If we enforced that, we wouldn't need any runtime checks when you add to a List. The unsafety arises because we allow the cast: var list = <int>[1, 2, 3]; (list as List).add('oops');
C# (and others) use declaration site variance annotations. Interfaces are invariant by default, unless you declare them as co/contra-variant. If you do that, the compiler will check to ensure you're using the type parameter soundly. So it'd be like class Iterable<out T> to indicate that T is always "passed out" of an Iterable.
There's also use site variance annotations, e.g. as used in Java
Why not though? DDC I think could be the defining difference between TS and Dart - it has both static and runtime checks so you can exhaustively unit test and e2e test your application, and once you are happy with the results you could optionally compile with aggressive optimizations. We could even document that you should run your same suite of e2e tests with your new ".aggressive.dart.js" code because we don't guarantee 100% semantics.
Unsoundness tends to work against aggressive optimizations. That's why it would be really good to get sound generics, so we don't pay any runtime cost for soundness, and we just get the optimization benefits. Also things like being able to make non-extensible or non-implementable classes/members would let us get more benefit from soundness.
@jmesserly I would say that current situation with generics in dart is huge soundness hole by itself and should be fixed regarless compiled to js code optimizations. What type safety can be talk about when I can do this: List<int> numbers = []; List<Object> objects = numbers; objects.add('string');. I am perfectly fine with either C# or Java way for dealing with generics, I would prefer C# but that's my personal background.
Also I want to add that we chose dart for being strict to allow us to scale for millions LOC, that ts with its fuzzy type system will not let easily do. So we are perfectly fine with strict (C#/Java like) generics, we are asking for it.
but we don't want to do hashCode and == checks for objects like protocol buffers
Dart2js treats string-keys special. If you see hashCode and == on the profile for maps with String keys, please file a bug.
@ranquild Note, this is a runtime error in Dart 2.0. (Right now, only DDC implements the check, but the intent is for all implementations to.)
List<int> numbers = []; List<Object> objects = numbers; objects.add('string');
@matanlurey I've been noodling on some ideas that are vaguely similar, so interesting to see this. We should talk a bit about this sometime soon.
@vsmenon That's good to hear, still, it is kind of error that type system should be able to statically eliminate. For example:
List<int> numbers = [1, 2, 3];
List<?> objects = numbers;
// objects.add('string'); // compile-time error
Object object = objects[1]; // ok
@ranquild
All generics in Dart 2 are covariant with soundness enforced by runtime checks where needed:
void main() {
List<int> numbers = [1, 2, 3];
var stuff = numbers; // stuff is inferred as List<int>
// stuff.add('string'); // static-time error
List<Object> objects = numbers;
objects.add('string'); // run-time error
}
Not sure if you're asking for different variance mechanisms and/or something like Java wildcards. Feel free to file a separate bug. :-)
@jmesserly Thanks for the ELI5s 馃憤
@leafpetersen Indeed. I'm chatting with some of your team this week in person, when you are back from your trip we will sync/I'll share notes.