Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Understanding null safety in Dart (dart.dev)
96 points by timsneath on July 25, 2020 | hide | past | favorite | 89 comments


I believe Dart is a more exciting language than Go. It's the most well designed scripting language out there, that can also be AOT compiled into native binaries. It's a better JS and a better Java at the same time. It would be also a killer language for backends and simple websites. Just imagine a better node.js built around Dart's excellent tooling.

What made Go more successful than Dart was a mix of decisions, luck, and momentum. On an alternate reality, Dart would swap places with Go. Dartium VM would have replaced Javascript on the Web and everyone would have been happy. Go would be seen as an obscure language with missing features that doesn't really do anything better than java/C#/C++


> I believe Dart is a more exciting language than Go.

Almost every language is, that's not difficult.


Dart is a better ES5, but a worse ES6. If you compare Dart to the JavaScript that existed when it was created, it’s great. If you compare it to modern JS, it’s crap. Modern JS is really, really good. The main problem with it is that the ecosystem is only just so-so. People still complaining about JS in 2020 are mostly just showing their own ignorance of what the language is now.


Dart is statically typed. I still miss the type system in JavaScript, and as much as I like TypeScript after I get it working, it has too many configuration options.


So that was your proclamation. Now let's hear your arguments...


The lack of a standard lib puts pressure on the ecosystem to provide simple functionality like leftpad for every project. Managing those is diffcult.


Left pad specifically is part of the standard library now: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

Yes, there are still shortcomings, but to me, the biggest shortcoming is too much legacy crap that doesn’t use the standard library. For example, Express predates the standard request, response, and URL classes, so it invents its own. Plus Node doesn’t even fetch FFS. If everyone actually wrote things in modern JS, the situation would be better.


> It's the most well designed scripting language out there

You misspelled Kotlin.


Strangely, both of you managed to misspell Julia.


Kotlin is statically typed only runs on the JVM though, I thought?



Sure, it’s theoretically possible. I don’t actually know anyone out there who’s using it, I’m curious do you?


> I believe Dart is a more exciting language than Go.

For certain types of tasks, you want to choose a language that is not moving fast and breaking things. Such a language tends to be less "exciting" than others.


Look at Common Lisp which fits this description much more than Go and is still a million times more interesting and powerful. Go just really is a below average language.


> which fits this description much more than Go

Was that true from the start or is it just that it's much older? How fast did Common Lisp move during its own first ten years, before the ANSI standardization?


Well, Go has only one implementation, right? So it's kind of always been standardised.


Funny, I find Dart to be horrid and verbose, and oddly daunting, mostly because of the expected massive surface area your code has to traverse, and these odd "let's pretend async is sync" erm "features" that the JS community decided was OK Koolaid to drink... I promise you a future that you can async await. Spare me. I want to know the truth, not a lie. Did that request return a 200 or what?

I don't want a better node.js nor any node.js at all, really. Node.js called and it wants it's browser back. It's an event loop, for crying out loud, and has no business trying to pretend that it understands operating system processes and threads and such...

Dart reminds me of the worst of large corporate committee-ware. Go is at least a decent and respectable programming language, even if it IS garbage-collected...

I suppose I will get flagged for having strong expressed emotions about Dart. I dislike working with it, immensely, and this was even before Flutter... let's please avoid discussing Android Studio, chip heat dissipation, and global warming... Clearly the Goog has no influence on the fact that you need 6 cores running at 1.whatever gigahertz to open Farty Birds or whatever is hip. Flag away. Sorry, "null safety"... is that like "if (cat ==== 'undefined') {break exception omg try catch}?"

.... What does Dart do better than it's nearest competitor? What does Dart compete with again?


Can you list your reasons to dislike Dart in a format without the vitriol?


Sorry, posting after a few drinks again...

Silly stuff like "I like wide-code-formatting, not vertical stacks with 4 characters on a line or something. (cultural)

"Too many moving parts" in almost every implementation of anything, (as in "modern" javascript/front-end frameworks and their build-toolchains) such that its built for a large corp to use lots of warm bodies to "divide et imperis". (I can understand this with regards to Flutter, half of the complication of this cross-platform thing being Googles fault, of course. Android Studio needing it's own dedicated 32-core terabyte-ram compute cluster, naturally, and trying to find cross-platform layout containers etc without being verbose = good luck!)

Particular pain points? Let's go with some Flutter stuff, as it's arguably the primary use case folks will encounter Dart for, now that the Dart VM isn't going to be embedded in all the browsers suddenly, as was proposed at one point.

Arguably, I'm doing it all wrong, as someone will point out. Elaborate (client-side) modeling that will only work if everything is completely fleshed out:

We have our classes, but OH let's make the state a separate object and let's basically have you haul 3 paragraphs of junk around to represent one thing and some related stuff that should probably be part of the same object... Hey, who knows, maybe it'll be like CSS in some future revs in which you have to look for 20 class blocks extending the same class, but that's in Crystal etc, and it's raison-d-etre seems pretty established (extensibility) but it does make localization a PITAS.

...and then we have factory methods, and we can't really debug passing stuff through them until we get data, but it's not as simple as if(this.readyState == 4 && this.status ==200) {boom, does it look like JSON? is it an array instead? what do we have here?} so there's this "build an elaborate model on the client side, but then you are basically only validating and attempting to JSON.parse whatever your (likely http) API spits out.

factory TextLine.fromJson(Map<String, dynamic> json) {} Never mind that I have to finish tiling all model bits to pull something through and see what I have, although the flutter hot-reload helps speed things up a bit, which you then end up pulling data through what pretends to be fancy/modern but is actually no better than the browsers HTTP XHR methods

//the future Stream<List<Album>> fetchCards(int limit, int skip) async* { http.Response response = await http .get('https://mobile.somesite.com/albums/'+ limit.toString() + '/' + skip.toString() ); List responseJson = json.decode(response.body); yield responseJson.map((m) => new AlbumCatalog.fromJson(m)).toList();

Syntax is generally fine, text is better than SVG, but somehow working with Dart makes me feel depressed, in a Javascript kind of way... (lot's of tooling and attempts to not be what it really is underneath, hence my gut reaction to arrow functions and pretend-classes in Javascript, etc...)


Does Dart2 offer any compelling features as a language that aren’t also present in Kotlin, Typescript, Swift, or Haxe? I know many users are interested in Flutter - but I’m curious more about the language itself. Whenever I see Flutter examples I’m disappointed I can’t use it with Kotlin (which already has many specialized backends) or some other language with more widespread support outside the Google ecosystem.


One of the big attractions for Dart is that it compiles to native ARM code on iOS and Android, compiles to Intel code for (today's) desktop platforms, and transpiles to JavaScript for the web. There are other high-level languages that support _some_ combination of these targets, but often there are significant differences between implementations (e.g. not all are of production-quality, differences in memory management or threading approaches, inconsistencies in implementation or APIs).


To be picky, and for those reading, Dart no longer uses the same "threading" approaches. Dart 1 used Isolates for the vm and web. Dart 2 uses Isolates for vm, aot, but web workers for the web. Of course with Dart 1 it compiled to web workers, but had the same Isolate abstraction.

Unfortunately, the Dart web worker implementation isn't very convenient and I think some work could be done there to make it nicer to use. It's a very important use case for today's devices.


I come from a TypeScript background, so I can compare it to that. The biggest difference to me was the soundness of the type system, where you're ensured (from a runtime perspective) that objects are correctly typed.

This cuts both way: it was great not having to think about typings all the time around input/output, but it was also cumbersome when implementing the internal implementation. I'm a bit biased because I've come to think in term of structural typing much more. Typing in Dart reminded me more of my time with C#.

Null safety was a huge missing piece though, glad to see it being added.


Is there any language that does both? I've really enjoyed js/ts's structural typing for prototyping, but there are certain places I miss nominal typing, particularly with input validation (and other "primitive type, plus restrictions") scenarios.

For example, an integer that's used to index into an array, so it must be positive. Or a string that's a valid street address. You can maintain that info by packing it into a unique structure (ie, wrap in an object, using a unique key), but that's awkward to access. Or you can always pass around the parent object (eg, House), which has a unique structure, but then you're introducing unnecessarily tight coupling which is a disaster to maintain.


If I understand correctly you want "new types" in typescript, which can be done either via a library or by hand rolling it:

- https://github.com/gcanti/newtype-ts

- https://github.com/Microsoft/TypeScript/issues/4895#issuecom...

but it isn't as nice as say Python's `typing.NewType` or Haskell's `newtype`



I believe classes are nominally typed in TypeScript. Otherwise, OCaml has row polymorphic records, polymorphic variants, and a structurally typed object system, in addition to a powerful nominal type system with ADTs and higher-order modules and such.


There are a couple of attempts in that direction with TypeScript[1,2]. A bit more work than if there was direct language support, but I've used "branded types" to distinguish between UserIds and ProductIds, between different types of currency, and between different unit systems.

1: https://spin.atomicobject.com/2018/01/15/typescript-flexible...

2: https://basarat.gitbook.io/typescript/main-1/nominaltyping


I find Dart comparable or maybe even slightly better than Kotlin, not sure why exactly, some things I like:

- Overall the language is simpler and more regular, it's easier to read it.

- Async / await is way simpler and less mentally taxing than Kotlin's coroutines.


Could you say more about your experience of Kotlin's coroutines vs async / await? I'd love to learn more about that! :)


Kotlin’s coroutines are mind-bending to me. They multiply all the spooky fears about threading correctly times a second order of semi-deterministic scheduling logic. The API surface around coroutines is also very complex. Javascript provides one simple API for creating a promise object, and a simple model for how async functions interact. On the other hand, Kotlin suspend funcs need to be “launched” or “run” on a coroutine context which may or may not actually allow async behavior, be backed by a thread pool, and transition between schedulers (?). Kotlin coroutines are very flexible but with that power comes a lot of confusing complexity.


It compiles to JS, ARM, and x86, for web, mobile and desktop support respectively.

Also, Dart has code generation so you can write your own language features, like the freezed package which implements algebraic data types even though Dart doesn't have them natively.


Yeah, I'm not a huge fan of the current code gen methods. They're pretty hackish and ugly. I understand it's prob the only option right now. Need partial classes: https://github.com/kevmoo/language/blob/5a1e560bb0871f07f682...


Does freezed feel nice in day-to-day use, or is it more of an incomplete/leaky hack?

I'm planning on writing a library in Dart that implements a custom TCP protocol, and I think ADTs would be the best fit for capturing all the valid states and error conditions. I really don't want to have a bunch of nullable fields or use the visitor pattern :/

I've been tracking this issue [0] and hopefully with the release of null safety, the team will have the time to implement ADTs!

[0] https://github.com/dart-lang/language/issues/349


Dart feels slightly less versatile than Typescript but has type soundness.

Both feel highly verbose to me, but maybe that's just cos I'm used to something with quite minimal syntactic overhead.

Ultimately, they're very similar as languages so it wouldn't take long for a Typescript developer to picn up Dart or vice versa.


Forgot what but I remember feeling quite inspired by some of the abstractions .. seemed just slightly innovative yet solving a problem too.


Null safety is absolutely essential for me. It's 2020, why are we still having NullPointerExceptions? However I'm curious about the next frontier of safety. We've slowly added a lot of nice features around safe use of values and safe memory. I wonder what's next.

Personally I've been thinking about safe use of arrays. Arrays are such an important data structure and yet they're horribly unsafe. Every time I do index math I get that same nervousness that I used to get accessing fields in nullable data structures. I'd love for a way to verify array indices that doesn't involve too much dependent typing.


In my view the next nice thing to have is algebraic data types with compile-type exhaustiveness checks. I believe several languages implement null safety using a simple algebraic data type that can be “Nothing” or “Some(‘a)” where “a” is a type. The compiler can enforce that you can never access the “a” without checking for both cases: “Nothing” and “Some(‘a)”.

That extends naturally to any number of types. For example, a type representing a network request can be “Loading” or “Loaded(‘data)” or “Error(‘error)” and you can’t just blindly access the data without also handling loading states and error states.

I absolutely love this when doing frontend work in ReasonML, and I miss it in JavaScript. TypeScript definitely has this functionality but I’ve never felt that it was natural to use.


I'd agree; if I had to guess what's most likely to become mainstream soon, it would be union types with matching (like Either or Result types). Strange that Go did the lazy thing and made the type signature a tuple, requiring you to match yourself...


Go explicitly eschews more advanced features in its type system–it not being "lazy", it thinks that such things are too complicated to be in the language. Most other up-and-coming languages have chosen to include proper sum types, including Swift and Rust.


> it thinks that such things are too complicated to be in the language.

Yep. Though, there are two ways to interpret that statement.

One of the things that has always slightly annoyed me about Go's messaging is that it conflates "complicated for the language's implementers" and "complicated for the language's users." To the point where it sometimes verges on being a sort of mild gaslighting.


I personally find languages that treat their users as stupid (providing facilities to the language designers but not its users) to be infuriating. Go is pretty much the paragon of this philosophy.


I'm not even sure that the intent is to treat users as stupid. It seems just as likely that it's just a collective experiment in elevating sour grapes into an art form.


Your more charitable characterization is certainly reasonable, but I find that pushing the complexity of matching the cases to the users (including the IMO invalid state of "returned a value AND an error") just results in a below-MVP error system.


Dart has compile time ADTs with the freezed package. I like Dart since it has code generation features so you can write or import custom language features, much like JS with Babel.

https://pub.dev/packages/freezed


I think the next frontier is things like Rust. I've griped about Rust in the past but I do think that the borrow checker often feels like I'm coding in the future - it understands my code on a much deeper level than any other statically typed language I've used.

I definitely agree with your feelings around array indices. I feel the same nervousness every time I do a slice or count backwards from the end of an array. I'm not really sure what the fix would be here, until I can just tell the computer what I want it to do, though...


I think the fix is dependent types, right? If the compiler can reason about the index and the size of the vector, that's one way.

For example;

let x = 5; let y = vec![0; y * 2];

for i in 0..x * 3 { y[x] }

That shouldn't compile.


Dependent types are cool, but I think the best fix would be refinement types. They are sort of similar, except refinement types use a SAT solver to prove correctness, while dependent types require to prove everything yourself (to the point where realistically something like what you posted probably wouldn't compile in most dependently typed languages). Refinement types also compose better.

Because of the SAT solver you have some restrictions on what you can prove, but they're so much more ergonomic I think it's more than worth it.


Sure, agreed that refinements are likely the better approach here.


Dependent typing can make guarantees like that if there are no loops, no parallel mutation, no I/O, no cosmic rays. For example if I made y have rand() elements then there's nothing a compiler can ever do about that. In other cases the compiler can check types by executing code (not always decidable). Dependently typed languages are not Turing complete. They can help with proofs and such but it's physically impossible to use dependent types for general purpose programming (e.g. try writing Linux in Idris, you can't).


Dependent types strictly just means being able to parametrize types with values. You can design many different types of PLs with this feature.

You don’t need strict reasoning for it to be useful like any other type level feature.


Typo: should be `[0; x * 2]`


Yeah, I caught it too late to edit :\


Yeah I've been writing a lot of Rust for my compiler and it's so much fun. But it still bugs me that indexing panics. Returning an Option would make a lot more sense...

What I've been wondering for my own language is whether I could design something like Rust's borrow checker but for ranges of values. That way I can guarantee valid indexing.


Vec does provide non panicking retrieval methods. You could newtype it and implement the indexing trait, delegating to the non-panicking methods.


But the advantage of that doesn't apply in garbage collected languages, right?


Actually, I wouldn't say so. e.g. Rust verifies that only one copy of my data is mutable at any one time. This is actually the kind of thing you'd always want to verify, even with a GC, because having two (say) classes that could mutate the same data instance could become a huge mess.

e.g. even in GC'd languages there are libraries to provide immutable data structures because we understand that having multiple writers to data is a mess. The true ideal (for coding ergonomics) is exactly 1 writer, but there was no way to statically verify that.


You'd like to verify this always, but the Rust compiler is limited in that it can only verify this at compile time with proper annotations. As such, it rejects some valid programs.


That there will exist valid programs which will be rejected is true of all type systems, and that you need "proper annotations" is true of almost all type systems (and if this was worded only slightly more broadly, to discuss "proper structure" or something, it would again be true of all type systems); this shouldn't be considered a theoretical problem unless it accepts invalid programs (though it might be considered a practical problem if it is nearly impossible to correctly structure/annotate a program to make it accepted).


> this shouldn't be considered a problem unless it accepts invalid programs

I mean, a compiler that accepts no valid programs is problematic is it not?

The way that the Rust compiler does its thing does not really work for a garbage collected language, and the comment that I was responding to, which was itself responding a restarting of what I just mentioned, said that you'd want Rust-style verification anyways for those things, which is not really an answer to the question because that kind of thing is not something that Rust can do.


I think twice about writing for loops. Functional style APIs make these kinds of mistakes way less common


Personally I'm a fan of list comprehension, particularly as implemented by rackets for/list forms. I certainly don't use them for everything, sometimes a plain old map fits the circumstance better, but I find myself using for/list fairly often. And I don't think it's error prone in the same way that a traditional C-style for loop would be.

https://docs.racket-lang.org/reference/for.html?q=for%2Flist...

    > (for/list ([i '(1 2 3)]
                 [j "abc"]
                 #:when (odd? i)
                 [k #(#t #f)])
        (list i j k))
    
    '((1 #\a #t) (1 #\a #f) (3 #\c #t) (3 #\c #f))


Statically typed metaprogramming. Ceylon is the only language that has that. No idea whether it's useful in real world though.

Also, one feature I would love to see implemented in a modern language is this:

    numbers..toString() // Equivalent to numbers.map(x => x.toString())


Also, one feature I would love to see implemented in a modern language [...]

In Raku (the language formerly known as Perl6), that is spelled

    numbers>>.toString()
or using proper idioms

    @numbers>>.Str


> numbers..toString() // Equivalent to numbers.map(x => x.toString())

You mean something like

    string.(1:5)
in Julia?


Yes. I really like Julia but I wish there was language that had all of the advantages of Julia but none of the drawbacks (for me it's lack of static typing, function(object) vs the more readable object.function(), 1-based indexing).


The indexing really isn't a problem in practice after a while. function(object) isn't just a readability thing, it's due to multiple dispatch which is ingenious. Lack of static typing, well... yeah.


Most of the problems I've seen that multiple dispatch solves can be solved with extensions.


And everything can be implemented in assembly, that doesn't mean that there aren't superior alternatives.


collections apis have solved this. i haven’t used an array in many years


But some things at the lower level still need indexes, don't they? Think of in place sort routines.


Yeah, maybe it's just my niche but I've used a lot of indices recently in my compiler. I have a bunch of tables for types, names, etc. and indices play a big role with them


map filter fold etc.


Not a Dart programmer, but looking at it for flutter. I see from Jetbrains Developer Survey[1] that Dart has grown to 9% from 6% last year[2] which I think is because of flutter, which has grown its share to 39% in cross-platform development.

But doesn't it seem reasonable to use TypeScript for flutter now? If you are not a Googler, Where do you use Dart (outside flutter) in production for a purpose where it is better than TypeScript?

[1]https://www.jetbrains.com/lp/devecosystem-2020/

[2]https://www.jetbrains.com/lp/devecosystem-2019/


>But doesn't it seem reasonable to use TypeScript for flutter now?

Flutter started as an experiment from chrome team, so their first language was JavaScript, when they move to dart they decided to write the whole framework layer in dart, the engine is in C/C++, so it seems there is no going back to a JS world.

https://flutter.dev/docs/resources/technical-overview#layer-...

With the Dart, they got a VM, GC and other tools and a language team that could grow Dart to suit flutter. They also got stateful hotreload, a Dart idea, which was something that the Flutter team didn't know they wanted.


I use Dart for my Flutter ios/android app and Typescript for my backend for the app. I don't think I'd even consider reversing that.

Why? Libraries.


For Flutter's needs (performant-enough JIT as well as stable AOT compilation for a variety of targets), TypeScript's (own) tooling is pretty much nonexistent.

Of course, the Flutter team could build the toolchains they need themselves. Or they could just...use the up-to-par language they're already using instead of rewriting things for no real reason.


I say this as a huge IntelliJ fan, ask anyone who knows me. My friends joke that I must be collecting commission checks, for as much as I nudge people toward IntelliJs awesome IDEs.

Take this survey with a grain of salt, because the results come from IntelliJ users, and IntelliJs most important and popular product is IntelliJ IDEA, a Java IDE (and other things)

So the results will skew in that direction for that userbase.


I understand what you are suggesting, considering IntelliJ products are tightly integrated with Android ecosystem there are chances that the survey results are skewed towards flutter development.

Then again, none of the responses here are outside flutter development!


Using Typescript for Flutter is the intent behind this project https://github.com/chgibb/hydro-sdk

Though, note I am the author.


I'm very happy about this - lack of null safety and implicit downcasts are two of the biggest problems with Dart/Flutter, which I otherwise really enjoy working in. I've gotten in the habit of adding "assert(x != null)" in all of my methods.


> I've gotten in the habit of adding "assert(x != null)" in all of my methods

Is this not useless most of the time, since you'll usually get an exception (with stack trace) when the value is used?


Yeah, but at least this catches it early - i.e. if a branch/if-statement uses the field, it might not always be hit when testing things out. Unit tests _should_ catch these things, but so should the type system, and for my side projects I'm kinda fast and loose with testing for better or for worse.


Dart is one of my favourite languages right now. I know it gets a lot of criticism for not reinventing the wheel and being heavily OOP based, but it's simple enough, standard library works well, IDEs work great. Also it actually has some unique features which I now miss from other OOP languages, such as named constructors - new Angle.fromRadians(...), new Angle.fromDegrees(...) - feels much cleaner and more self-documenting than the static factory method other languages have you use.

Optional typing I never really cared about, because even in languages with var I find myself writing the types whenever possible.


I used flow typing in MyPy and it's awesome. TypeScript has the same thing (although I would be curious if their rules are significantly different).

I think you can't practically have null safety without flow typing, at least in an imperative language, otherwise you end up with casts everywhere. MyPy has null safety, though it takes some effort to turn it on for most codebases.


Not at all - that kind of dataflow analysis is basically just a hack to patch over the fundamentally broken design of having everything be nullable. It's totally unnecessary in a language with proper algebraic data types where there's no all-pervasive concept of "nullability". The only imperative language I know of that has this is Rust, but it originates from ML languages like OCaml and Haskell.

It's one of those features where once you get used to it, it's hard to fathom how anyone programs without it. It's such a simple yet expressive feature that's incredibly useful when writing normal, ordinary code. Nothing fancy or theoretical.


I'm mostly agreeing with both you and the sibling - flow-sensitive typing is great. I think it's awesome in Kotlin, and removes the Java-style casting or manual unwrapping inside of if guards you always end up with.

As sibling said, ML-style languages get away with this a different way - chained "container" operations in Scala, for example, are "right-biased", so the only get called if the result of the preceding operation was a success or not null.

I've heard it called "Railway Oriented Programming": https://fsharpforfunandprofit.com/rop/


I'm really hoping this will make AngularDart a lot nicer to write. Inputs might have a null value and it's very easy to forget to account for it, will be great to catch that at compile time.


Looks to be very close to the solution Zig also went for. I like the solution and it seems to fit both languages very well. I should look a bit further into Dart some time I think.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: