The road to Dart 3: A fully sound, null safe language
medium.com
medium.com
Null Safety is the killer feature of modern programming languages like Swift and Kotlin. It provides a clarity you just can't get otherwise and saves you from runtime errors. And it requires very little additional ceremony to use.
(edit: removed incorrect statement about Python's type safety)
https://docs.python.org/3/library/typing.html
https://github.com/python/mypy
> let alone null safety
https://docs.python.org/3/library/typing.html#typing.Optiona...
The fact that the source can contain completely wrong declarations and Python will happily run it anyway felt really bad. It's kinda like JavaDoc rather than a classic type system.
You can live with it, but most language eventually add some way to deal with it. Java has @NonNull. Javascript and Python both have static type annotations that are non-nullable. Even C++ has some attempt at it (std::option<>).
The only one I know of that hasn't bothered trying is Go.
Yes, we debated it for years.
Literally the day we launched, a user filed an issue requesting support for null safety: https://github.com/dart-lang/sdk/issues/22
For most of Dart's history, that was the #1 upvoted issue on the issue tracker.
Back in 2011 before I worked directly the language, I proposed null safety:
http://journal.stuffwithstuff.com/2011/10/29/a-proposal-for-...
I'm immensely glad we finally did it, even though the migration has been a ton of work.
- It's super-easy to create a situation where some some obscure code fragment can cause a panic and crash the whole app due to nil pointer access, especially in multithreaded scenarios
- We had to start using linters to track possible nil errors early on
- Eventually we just moved to Kotlin, where this is not an issue
- And don't get me started on nil interfaces, good luck properly checking nilability and tracking these kind of issues
For mappings between objects’ deeply nested properties I found mapstruct to be the best tool which will handle it correctly either way.
I am a java guy since 1.1 and it seems to me that a lot of problems in Java come down to it being designed for a time when a application mainly consisted of in-house / in-org / self-written code. In $currentYear, however, we mostly plug libraries into frameworks and it's just a pain to check every call and every return value for the possibilities of null. Yeah, it can be done but I just don't like having to read library source code to find out if it will return a null in some cases.
Nulls, arrays degenerating into pointers (losing size information), and accidentally crossing enum types across libraries were all huge sources of bugs. Java doesn't have much of an excuse for its treatment of nullability, other than they were trying to court C/C++ developers.
Which one is the standard, the dormant JSR-305, the IntelliJ annotations or the checker framework (I guess that's the best candidate since it was actually accepted)?
> I found that defaulting to everything non-nullable (there is a setting in these tools which should be the default, implicit nullable or non-nullable)
Does this have a good cross-IDE support?
> gives you effectively the same null safety as Kotlin
"effectively", just ergonomically way worse.
But the main issue I have with this is that it requires you to actively seek null safety, pick a particular "standard" and push it through, ruthlessly enforce it during a code review. Which mostly doesn't happen. I've never seen a large codebase (which I would work on) which would use such annotations consistently. Yet unsurprisingly, all Kotlin code bases I've seen were in fact null safe.
Hence “de facto”. All of these tools understand all of them.
And I do have to agree with your last paragraph, it is not as good as native support, but nor is it as big of a pain point as it used to be/as some people think it still is.
It's a big enough pain point that I resigned myself on null-safety in existing Java projects.
I rarely have the opportunity to start projects from scratch, and for those I'd rather use Kotlin.
Not really, I'd say not even close. In Java you would be trying to avoid nulls whenever possible, but in Kotlin nulls elegantly become a flow control feature.
For all of its flaws, at least C++ made null references undefined behavior. Coming from (pre-NotNull) Java, it's nice to avoid lots of defensive null-check boilerplate.
NotNull, or so called "null safety" makes it that when you know something is not optional, if a null is passed to it, you know it's a bug.
But when you do have null again, by making something nullable, the million dollar problem is back, in that you don't know why it's null?
Is it null because it's missing, false, errored, eof, leaf node, etc. You don't know.
In my opinion what we would need is union types, and simply never use null for anything, remove it completely.
Instead you could declare a function returns a Value or Missing. Or it would return a Value or False. Or it would return a Value or EndOfFile.
And similarly you could say:
User {
String|NotProvided email;
}
Where email on user can be either a String or NotProvided.You wouldn't want the NotProvided, EndOfFile, and all that to be full on classes though, just like type aliases of some sort, similar to null in a way, but it adds semantics.
if (email == NotProvided) {
...
}
And the language compiler would error if you try to use it where it doesn't work without checking: String email = user.email; // Error
if (user.email != NotProvided)
{
String email = user.email
} // No errorSo, for a quick script or rapid prototyping, just unwrap/panic if you don't care about the result, like in a short code contest, and for any other use, refrain from using these, match all possibilities, implement a strongly typed Error management.
In Kotlin (and others) it's extremely ergonomic, too. You just say foo?.bar and coalesce with ?: so
val baz = foo?.bar ?: "foo or bar is null"
In certain cases the Kotlin compiler will infer non-nullable type after an explicit null check just like in your example such that
fun bestFunction(foo : String?) {
if(foo == null) return
foo.baz <- //now legal to call without .?
}For Ops requirement I think it's too specific for a language level feature. In Kotlin you'd just use a sealed type in the cases were you really need to know why function can't produce a value. Works like a charm.
But maybe I'm the only one who's bothered with code generation :D
i have a large set of scripts i use to automate the boring parts of my job. i originally wrote them in bash. past a certain amount of complexity, that became unworkable, so i ported it all to python. after about a year, i found python to be too limiting as well, based on my own coding style, so i am porting it all again, this time to dart.
the dart compiler makes it a cinch to create command-line apps that work on basically anything with a terminal.
import "package:crystal/crystal.dart";
super easy. i can add it from memory, or copy an existing module in this package, which will already have that line at the top. it is extremely rare that i ever need to modify this, or add a second line to the import section.meanwhile, let's say i am adding a new python source file to a package. the import section will end up looking like this:
import os, base, dart, execute
from typing import List, Optional
from .build import build
from .build_args import BuildArgs
from .build_type import BuildType, buildTypeList, buildTypeParse
from .build_type import buildTypeStrings, buildTypeValid
... and pretty much every change to this source file will require modifying the imports section, in ways that i definitely have not memorized, even after years of python programming.trying to write a large program in python was, for me, death by a thousand paper cuts like this, which don't occur in dart.
The community has lots of great, well built packages, and the Dart and Flutter teams are also quite responsive to the community, and helpful when an issue does arise. I've spent several threads going back and forth with maintainers trying to figure out a bug, and I'm happy to say that every single time we've found a solution.
I really feel that the way forward is something like Graal running multiple languages in the “one VM to rule them all”.
In summary, if they are that convinced sound null types are needed, to me it needs better error handling that matches the same level of discipline. That said, any and all improvements are always welcome, so I do think overall it is cool they are taking this seriously.
Swift has made large backwards-incompatible changes 3 times now.
It's much more stable these days, but personally I'm still wary of the team's (past?) proclivity to make sweeping incompatible changes.
The other thing you don't see much in Result heavy code is coalescing multiple errors (like for example, most parsers in Rust just bail out at the first error when you want all the errors at once). It's possible to write code that does that, and exceptions have the same problem, but it's still a bit cumbersome.
The real frontier of error handling is algebraic effects, or resumable exceptions.
2. Unlike Go's, rust's panics do not actually, universally, get "propagated up the call stack until they reach a "catch point", where they can be handled by the programmer". There's a compiler flag which can be "unwind" or "abort". In the former case (the default), panics can be caught and recovered from. In the latter case, the program gets hard-stopped on the spot.
3. For 99% of users, panic = unwind in rust. If you play with compiler flags, C doesn't have undefined behaviour because ubsan will abort programs if you compile with the right flags.
There are Rust libraries (e.g. salsa, used in rust-analyzer itself) that use unwinding internally for non-local control flow and won't work with unwinding disabled.
Now don’t get me wrong, Java’s implementation leaves much to be desired, inheritance is not the correct choice for denoting it, but I feel we really didn’t give it a proper choice.
You're speaking from a place of general philosophy, but in practice (at least in my experience), the great majority of unchecked exceptions come from null references. In the TypeScript codebases I've worked on, the only runtime exceptions we really have to think about come from network requests and (unfortunately) JSON.parse(). You quickly learn to always handle those two cases, and with that we see basically zero unhandled errors in production
Of course we're talking about Dart and I don't know how commonly Dart throws unhandled exceptions or where they might come from, but I think it's a dramatic over-simplification to suggest it's not even worth trying to reduce errors just because unhandled exceptions are technically allowed at the language level
For quick scripts I still reach for python or Linux: bash, Windows: AHK. But if I'm developing an app I now reach for Dart/Flutter.
One of their headline libraries, angular dart, pushed out a broken release that stayed broken for nearly a year.
Dart was revived with flutter and I'm glad it's doing better. However, I have trust issues with the way google runs their projects. I simply don't know if they'll continue to support Dart/Flutter or if they'll drop it next year for something shiny.
I also don't appreciate the gaslighting that happened on the dart forums. There was a lot of "Oh, dart isn't dead, we are using it heavily and actively in google!" that happened when dart was very clearly abandoned by google. They pulled the exact same stunt with GWT before replacing it with J2CL.
Dart/Flutter are highly unlikely to be abandoned.
That's the period that left a bad taste in my mouth. IDK who chose to use Dart for flutter, but by doing so they revived a basically dead language.
[1] https://techcrunch.com/2015/03/25/google-will-not-integrate-...
Sorry you had poor experiences. :-(
That said, it was true that Dart was used heavily internally in that time frame. It was and still is, by Ads (ads.google.com) - that's a non-Flutter Dart app and still (to the best of my knowledge) the largest Dart app around. Before Flutter, we went through a period of primarily prioritizing internal customers. That work was often not visible externally.
We did do the shift to Dart 2 in that time period though. That was a fairly massive change to the language that began independently of Flutter. It was a nice timing that Dart 2 shipped at the same time as Flutter 1.
Let me be blunt, the work was never visible externally. What was visible externally is major language designers leaving the team (Lars Bak), Updates to the language slowing or stopping all together, the chrome team deciding to abandon dartvm in chrome, the angular team deciding to use typescript for angular 2.0, and Dart angular being abandoned (even as it was still being advertised on dart.dev!).
What other conclusion was an outsider to draw other than "Ok, google must be done with dart"?
What was the plan if flutter never happened? Would google ads continue to use dart?
Like I said, happy that you and your team are now enjoying some nice popularity. I jumped on dart early because I thought it was overall a good idea and decent language. However, once bitten, twice shy.
I'm really sorry you got burned by the early experience. It was a hard time for Dart users. It was a hard time on the team too. It felt like we were wandering in the wilderness for a while and struggling to agree on what language our users wanted us to build for them.
I think where we've landed is a much better product, but I'm sorry that our churn getting there caused you pain.
And I really appreciate you saying this. I get that what I'm saying is harsh and probably comes off as whiny/ungrateful. I'm mostly just venting because of the original "Why are people mad about dart in HN" comment and explaining my position there.
Dart does look like it's moving forward in a good direction and I wish it all the best. It's certainly great that the language and ecosystem are seeing a fair bit more adoption.
I was surprised to see Dart 2.0 come out and Dart 3.0 has surprised me even more.
And based on Google's track record, Dart is at risk of being cancelled any day without any notice.
You can also use AngularDart if you want but it's less common than Flutter Web in my experience.
Flutter on the other hand, for all three targets, it Just Works™.
Packages didn't break on Flutter, releases are quite stable and preserve backward compatibility, at least generally.
I can't understand how this can be true when JavaScript/TypeScript exists, but I'm open to religious conversion. Can you share details of the desktop/mobile/web app(s) you've created with Dart?
Another mental block for me is the association with Flutter, since every Flutter app I've tried (iOS and web) feels uncomfortably strange.
A family member was after a bespoke app that would do some calculations for him on site and include pictures and customer details. They wanted it to work on mobile and desktop.
At this point I thought a web or hybrid app would be the way to go. One code base multiple platforms. Cue a month of trying to work out what to use out of an uncountable number of frameworks.
I can't overstate the level of confusion involved it this for a novice like myself! I've created a few simple desktop programs before but nothing on mobile or the web. Where do you start with the current state of things on the web? I still don't know. What I did know was I didn't want to waste time learning a framework that wasn't going to be around, or had just appeared.
At this point I heard of Dart and Flutter which worked across all the platforms I was interested. Not only the it was JIT or AOT when needed, with hot reload.
As for the app, I don't have a GitHub, it's so bespoke to its usecase I don't think anyone would be interested. But also I'd be very embarrassed to release the code. I dread to think how bad it would look to real programmers/developers!
Congrats on overcoming hurdles and delivering a product. Not all professional developers always achieve that.
Each of these layers is public API and follows the same breaking change process as any other part of Flutter's public API, so with a bit of effort an alternative imperative framework could conceivably be built built on top of what's there. The downside is that since the widgets layer is reactive, you'd be giving up all those shiny widgets that are part of the SDK and need to build your own (or find a way to wrap what's in the SDK).
This (old, but still accurate) talk by hixie covers the layers in detail: https://www.youtube.com/watch?v=dkyY9WCGMi0
TL;DR it's entirely possible to create an imperative framework on top of Flutter's lower layers, re-using a lot of our existing code, but (as far as I know) such a thing doesn't exist today.
It doesn't help Dart that Kotlin and Swift have been null safe languages for a decade now.
https://stackoverflow.blog/2022/02/21/why-flutter-is-the-mos...
I mean…
Take a look at job boards and see how many job postings you find for React compared to Flutter.
For anything I might do in Dart, there's a better alternative. For backend stuff, I'm going to write in Java or Kotlin. For frontend stuff, I'm going to write in JS or TypeScript. I like Flutter, but Flutter by itself isn't reason enough to adopt Dart.
Long term, if Jetpack for Desktop actually takes off, I don't see any reason to write in anything but Kotlin.
https://github.com/dart-lang/language/blob/master/accepted/f...
Dart is a pleasure to work with, I'm not really a CS guy, but it's like Typescript with real types. Better tooling, no NodeJS legacy crap barely holding itself together.
I don't know if it'll ever unseat React on the Web though. Once an ecosystem has momentum it's hard to disrupt.
Just because some dude with a fancy name said so doesn’t mean it’s true.
In particular, how many dollars does it cost to have null safety, and how many more does it cost to use it? I feel like folks quoting Hoare never even bother asking this question.
That exact syntax (modulo langage divergences) is one of the options for “nullable” in Python: https://docs.python.org/3/library/typing.html#typing.Optiona...
The typechecker would only allow on the union operations which are allowed on both types, anything beyond that would first have to use type checks or assertions in order to split the union.
So, again, what are you talking about?
There are some exceptions to the behaviour you describe, like for instance with C# which for the longest time only allowed value types to be annotated as nullable, and only very recently extended this to reference types, and only as an opt-in feature, and the type checker only throws warnings, etc. That would be a case of a language which is not "null safe" but provides faculties for accomplishing that.
Unfortunately, there's a lot of cultural momentum in this regard.
Claiming that a reference cannot be null means that if you later realize it has to be, you’ve got a potentially compat-breaking refactoring to make.
So, nullable-by-default is either a good idea or a bad idea depending on lots of complex reasons.
Without null safety everything is implicitly maybe null.
In type theory, a type that is a supertype of all types is called a "top type" and a type that is a subtype of all types is a "bottom type". It's okay to have one or more bottom types in a type system, as long as it's not possible to create instances of them. Unfortunately, in most of the popular statically typed languages, null is an instance of a bottom type. One reasonable use of a bottom type would be to allow a variable to assume one of many types, and require a dynamically-checked upcast when actually passing that variable to a function that cared about its types.
The problem became this additional function color; also, no language construct can prevent users from implementing "fun Result<T>.unwrap(): T" and crashing the program in the failure case.
OK, you could also restrict functions that accept Result<T> (as a receiver or parameter), but now you force resolution of that type by the direct caller of the function returning Result<T>. So really this becomes a worse version of checked exceptions, which (thankfully) don't exist in Kotlin.
The null-safe language I've used the most is Rust. It is far from the hardest part of the language to learn, but the biggest impact day-to-day is that when constructing a new object, initial values for all the non-nullable fields have to be provided all-at-once. You can't have a constructor which is passed a `this` object full of nulls then fill in all the values later, instead the object is constructed whole in a single operation.
Otherwise the explicit distinction between T and Maybe T just makes life easier.
I think the sentiment behind the quote and its widespread use is correct however. It definitely does seem like the tradeoffs of null safety are worth it in the vast majority of cases. Most of us who use GC languages with massive runtimes sacrifice a lot more performance for a lot less value.
Not necessarily. If Hoare had not used null references he might have invented nullables to fill the need and others would have replicated that.
I think this can be supported by the fact that these fancy language features have been successfully implemented for decades and are only now showing up in mainstream languages.
Neither "simplicity" nor "performance" were huge factors in Algol's design.
Nonsense.
The entire point of Hoare's design was to have type safe references, which C pointers most definitely are not. The entire point of references was that they not be pointers.
Close to none.
>Just because some dude with a fancy name said so doesn’t mean it’s true.
The dude is responsible for tons of what we take for granted. He is also the one who invented the null pointer, so there's that.
I'd take his expert opinion and experience on the matter over anybody referring to him as "a dude with a fancy name".
He didn't sit to calculate the money in damages from it's use - it was more of a figure of speech and a guess ("probably").
But it's very likely that the prodictivity losses from null errors, the consequences for security, program downtime, and so on, could trivially have costed 1 billion dollar in damages over the years - actually much more.
A single null error in one of the space code responsible from bringing down a NASA mission, could easily cost close to a billion itself.
In fact the memory safety industry (e.g. static analyzers and in memory analyzing tools), safety checks for nulls by consultants, and so on, industry easily has have 1 billion in profits over the last 40 years, so there's that...
so zero dollars were spent adding null safety to languages? False.
Zero dollars were spent converting code to those languages? False.
Zero dollars were spent solving problems uniquely created by not having the benefits of null? Also false.
> I'd take his expert opinion and experience on the matter over anybody referring to him as "a dude with a fancy name".
Sounds like a crappy way to reason about facts. Just because he did cool shit once upon a time doesn’t mean that we should believe him when he pulls numbers out of his ass.
I could also do an immitation of a dumb pedantic person and just answer your "First, zero dollars were spent adding null safety to languages? False", with: "I didn't say "zero" I said "Close to none". So PWNED! or something equally immature.
But one can also chose to be charitable. E.g. to understand that:
> Zero dollars were spent converting code to those languages? False.
Having null safety doesn't necessarily mean "converting". Could also mean not having nulls in a language to begin with. So "how many dollars does it cost to have null safety" in that case is zero. It's fixing the addition of nulls after the fact that can have a cost.
Even so, the conversion to null safety is not that costly (it's nothing like a rewrite, more like going around a program and fixing SQL Injection cases or XSS). It also makes evident many logical and safety errors in the initial program when done (many teams have written about such experience), and any cost has to be offset with the cost of those errors not happening anymore.
In any case, those are not costs of not having null to begin with in the language. They are costs of retrofixing code in ones that did have it.
> Zero dollars were spent solving problems uniquely created by not having the benefits of null? Also false.
There are no "problems uniquely created by not having the benefits of null". In fact, there are no "benefits of null" to begin with.
> Sounds like a crappy way to reason about facts.
Then again sounds like you didn't provide any facts. Just shown ignorance of computer science theory AND history, along with certainty and immature tone.
This one doesn't make any sense in the context of the languages being discussed. What unique problems does it create in Swift/Kotlin/Rust?
Do tell what those problems are? Whenever I hear people complain about this, they suffer from some fundamental misunderstanding of how null-safe languages work in practice
You literally tell it to break, a null-safe language will not actually prevent you from shooting your foot, it'll just make sure you do so knowingly. "foo!" is essentially a shortcut for
guard foo else {
fatalError()
}But at least it did explicit (you HAD to add those characters) to get the unsafety than in C is the default.
Is it? I can’t think of any langage but Elm which doesn’t have that assertion. Though I guess e.g. Idris or ATS could have left out as well.
Personally I don't really see that as an issue, we're talking about null safe by default vs not safe by default. It being simple to break the null-safety is a good thing, as long as it requires you to do it deliberately
null pointer is more like a safety belt silencer. Convenient, especially when you don't want to be bothered or listen to the warning "beep". But there's no actual healthy benefit from using it.
That said, languages not providing a first class fast fixed precision/rational/decimal implementation for where precision is required, and this having been relegated to niche libs, is indeed a mistake.
But anecdotally I've experienced many null-pointer runtime errors, to the point where I very strongly believe having strict or even half-decent null checking reduces the amount of runtime errors my code produces non-negligibly. I'm sure not everyone has the same experience, but I strongly prefer languages with null safety (AKA marking a type "non-null", and then the compiler makes a good effort to ensure it's not null).
I'm sure that I've spent weeks worth of time doing this in one form or another and I doubt that I'm alone here. By that metric, it's more like the 100B mistake.
It was a simple one-liner in his type checker "if getting a type error in a cast, if the source type is nulltype and the target type is any kind of reference, always let the cast happen". His (biased by hindsight) recollection is that he even felt it was a bit dirty at the time, but made a lot of code shorter.
The billion dollar mistake was not null, it's languages that have a null value but don't support nullability in their type system.
Kotlin and Swift get this right.
Nullability/optionality is clearly useful. References/pointers are clearly useful. The problem is making them a single concept in a type system, thus requiring all references/pointers to be nullable and making it very awkward (such as forcing the user to manually declare a wrapper type holding a boolean and the wrapped value) to have optional values without making them references.
The conflation of referentiality and optionality in the most popular static type systems also bleeds over into mental shortcuts used by many programmers in dynamically typed languages when thinking about the types of parameters expected by functions/methods. Had Algol W, C, etc. kept optionality and referentiality separate in their type systems, I think Python/Ruby/Lisp etc. programmers would likely think more carefully when passing None into fuctions/methods.
Optional parameters in Lisp take on a value of nil when the argument is omitted (unless a different default is specified). Various search functions return nil when an item is not found: find, member, gethash, ...
The practices around these conventions may, at times, resemble work in static languages with null references, but there is no cause-and-effect there.
It's not that null as a concept is a mistake, since a Option type has both Some and None, it's that in most mainstream languages, people have to implicitly deal with them rather than having the compiler checking it for them explicitly. And if the computer can check our work for us, why do we have to do it ourselves?
That's why it leads to mistakes and is why it's called a billion dollar mistake, because I'm sure at least 1 billion (possibly even 1 trillion) dollars worth of manpower, lost revenue and time have been spent dealing with nulls.
[0] https://en.wikipedia.org/wiki/Option_type
[1] https://en.wikipedia.org/wiki/Standard_ML#Pattern_matching
I don't have any particular side on this battle, I can use both approaches. I just don't think that it's a big deal.
To me (with my limited knowledge on Dart), it feels like Dart is lacking a lot if it wants to work as a systems language.
The reason they spouted was due to "Low developer resources" due to the difficulty of C, so they swapped to a language pretty much only used by Googlers. Google now has control of sass, which then gives them greater influence (even greater than just having browser majority) over CSS Spec choices (see CSS Nesting spec).
Now I know that actually it gives me liberty to safely use null wherever I want and not have to worry about it blowing up in my face later.
The keys to this is that the language makes it obvious what can be null "Type?" and by giving me ergonomic tools to handle nulls such as ? and :? and compiler non-null inference. I wonder how this pattern of
make X visible and give ergonomic tools to handle X
could be applied to improve other aspects of programming.
Dart itself is used heavily on the Web by Google. E.g., ads.google.com is a Dart web app - but not a Flutter one. It uses a Dart version of the Angular framework. It's probably the largest Dart app in existence today.
Flutter on the Web is less mature, but (IMO) making good progress.
Big fan of Flutter though, what's next for Flutter on the web, anything interesting?
https://medium.com/dartlang/angulardart-flutter-and-the-web-...
TL;DR - It's still heavily used inside of Google, but the team decided to stop maintaining the open source version of it. It was a fair bit of work to do both - different build rules, different tests / test infra, different priorities. Effectively, AngularDart has been forked. There is an internal-only version that is actively developed, and there is an external community project.
Regarding Flutter on the Web, there is a lot of active work, but I'm not the best to speak to all of it. On the Dart side, it's one of the major reasons we're investing in things like compilation to Wasm.
Just had another question, I actually just filed a feature request for Dart (based on reading this thread about Option and Result types) about whether Dart has a Result type [0]. Looks like it does in Flutter's async module, but I wasn't sure why that wasn't also brought to the rest of the language.
And for WASM, I thought garbage collection for WASM wasn't stabilized yet, will Dart have to wait until then? What benefits does WASM provide for Flutter that's not already covered by how it does web support anyway, ie drawing inside a canvas?
double calculateArea(Shape shape) => switch (shape) { Square(length: var l) => l * l, Circle(radius: var r) => math.pi * r * r };
Aka:
extension Area on Shape { double calculateArea() { ... } }
extension on Shape {
double calculateArea() => switch (this) {
Square(length: var l) => l * l,
Circle(radius: var r) => math.pi * r * r
};
}
example(Shape someShape) {
print(someShape.calculateArea();
}
Extension methods are statically dispatched, so they don't give you any way to write code that's polymorphic over the various subtypes. In order to have code specific to each subtype you need something like virtual methods (which requires you to be able to add methods directly to those classes) or type switches (as in the pattern matching like we have here).It's really hard to do both approaches. (The fact that Java has an Option<T> type, but all references to it are also nullable, is sometimes a source of chagrin and sometimes hilarity to me.) If you want a nice user experience, consistency across the ecosystem is huge.
So when we decided what to do about statically checking for null reference errors, we went with nullable types (the same as Kotlin, TypeScript, and C# do) instead of option types (Swift, Rust, etc.). I wrote a long thing about my thinking on it here if you're curious:
https://medium.com/dartlang/why-nullable-types-7dd93c28c87a
It's a real trade-off. There aren't perfect solutions. Option types have some nice composability properties, but they tend to be more verbose and don't play as nice with the imperative control-flow heavy code that's idiomatic in C-derived languages.
Even putting null-safety aside, the latter part of the post with destructuring tuples and pattern-matching shapes looks almost 1:1 like SML code!
Kinda.
The trap with putting out 'crap' and fixing it later is that language designers will inevitably try to preserve backward compatibility, meaning the crap has to stay in forever.
Putting null and null-safety into a new language is like putting venereal disease and condoms into a new language. People will spill paragraphs on whether or not condoms are worth it, and whether VD is a problem if you have a sufficiently advanced IDE. But why put the VD into the language in the first place?
Languages where you can express if something can be null or not are a joy to work with and remove a whole class of errors.
That being said, I still don’t really understand the value proposition for Dart. Through different versions it’s morphed in the classic solution-looking-for-a-problem way (eg the optional typing). Did we really need this when Java, JavaScript, Python and Go existed?