scala-js / scala-js-dom

Statically typed DOM API for Scala.js
Other
317 stars 162 forks source link

Null handling #481

Closed japgolly closed 3 years ago

japgolly commented 3 years ago

What do we do in cases where a type in facade is nullable? We have js.UndefOr but sadly there's no js.NullOr. In scalajs-react I add | Null to facade types but the problem is it's non-trivial to get rid of the null (outside of Scala 3's experimental explicit-nulls), and I've had to add helpers with casts.

I absolutely hate the idea of just saying AudioTrack and the user having to somehow know that it's nullable. I want more clarity for users: types would be the best, information is probably the fallback (eg. annotations, worse: doc).

What do to? It might be an idea to add something like NullOr to scala-js-dom itself.

armanbilge commented 3 years ago

Yes, I ran into this too when reviewing a PR. The idiom here so far seems to be not to indicate the possibly null type. OTOH, so far this library has essentially been a pure facade and adding opinionated nice-ities is arguably out of scope?

armanbilge commented 3 years ago

Btw, https://github.com/scala-js/scala-js-dom/issues/414#issuecomment-646650961 made me think critically about what the purpose of this library is. I don't mean to open a can-of-worms over null but it is helpful if we clarify the goals/scope of scala-js-dom going forward.

japgolly commented 3 years ago

this library has essentially been a pure facade and adding opinionated nice-ities is arguably out of scope

I agree with that sentence but I don't agree that clarifying nulls in types goes beyond that scope. In the same way the js.UndefOr is a facade over A | undefined (with a few helper methods for nice Scala usage) I think a similar concept over null is just as in scope. It adds precision to the facade types.

Plus I'd also argue that in addition to simply being pure facades, we also provide utilities for convenient Scala usage, and there's already precedent for that. (Check out the [S_] tagged items in the api report.) As an example, one of the most annoying things ever, is getting a NodeList back from a facade, and not being able to iterate over it via typical Scala collections. I think I saw somewhere recently that this is fixed in our pending PR queue? But regardless, I'd argue we've got to go beyond providing the most minimal use. I think we should hard-limit our scope to JS DOM and browser API, but within that scope I think we should make it as nicely usable as possible, and as safe/precise as possible.

japgolly commented 3 years ago

Re #414, yeah I don't know about generating this code from TS. Seems like a great idea if everything's up-to-date, and we have a means of overriding certain generated decisions. It's interesting but I'd also say it's orthogonal to the goals/purpose of this project. Like regardless of whether we hand-write or generate the code in this repo, we still need to make decisions about how to make the facades as precise as possible, and ensure they have good-enough dev ergonomics, so I'm happy to separate the decisions.

armanbilge commented 3 years ago

I think we should make it as nicely usable as possible, and as safe/precise as possible.

So, why not return Option for example? I guess I'm confused where the line is drawn.

japgolly commented 3 years ago

Option is a Scala type, we need a type that behaves like Option but represents X | Null at runtime in JS world. Option is None | Some

japgolly commented 3 years ago

Plus different subtle semantics too, like you can't represent Some(None) as X | Null because it would be reinterpreted as just None.

armanbilge commented 3 years ago

Plus different subtle semantics too, like you can't represent Some(None) as X | Null because it would be reinterpreted as just None.

Right, but as long as we rule out nested Options I don't think that should crop up :)

I guess the question I'm asking is, what's the difference between having a facade that uses X | Null but then has an implicit conversion to/from Option for convenience sake, versus defining the interfaces in terms of Option to begin with and implementing them via a raw facade hidden from users.

japgolly commented 3 years ago

Option isn't a JS type. It's a sum type of two Scala classes None and Some. From a JS pov, having two Scala classes is not the same as having a nullable JS type. Eg. None != null and Some(1) != 1. Theory aside you basically can't use Scala types in facades as a blanket rule.

We could have an implicit conversion from A | Null to Option but I don't think an implicit conversion is a good idea. It's non-obvious, users have to Just Know that they can call option methods on a non-option, plus they're require a special import (which I'd like to avoid as much as possible).

The reason I'm wondering about NullOr is that it wouldn't require any imports, it's mandatory and explicit, and it would be identical to js.UndefOr which is a pattern that's in place already everywhere.

armanbilge commented 3 years ago

Apologies, perhaps I'm misunderstanding, but js.UndefOr[A] is just a type alias for A | Unit, so then wouldn't be your proposed NullOr[A] just be A | Null with all its caveats?

armanbilge commented 3 years ago

Theory aside you basically can't use Scala types in facades as a blanket rule.

Right, of course it doesn't go on the facade itself :) absolutely not! But in theory this library could provide a higher level interface with Options rather than a raw facade.

japgolly commented 3 years ago

I haven't thought deeply about the solution, I'm just clarifying the problem and the limitations here :) But yeah if we end up with just a type alias over X | Null then I want to ensure that whatever's been done to make js.UndefOr function as more than just a type alias, get's done for NullOr too.

armanbilge commented 3 years ago

that whatever's been done to make js.UndefOr function as more than just a type alias, get's done for NullOr too.

See my comment above about implicit conversions ;)

armanbilge commented 3 years ago

Although you make it good point, it's an implicit conversion to add syntax rather than an implicit conversion to another type. Important distinction, my bad!

japgolly commented 3 years ago

But in theory this library could provide a higher level interface with Options rather than a raw facade.

That's a nightmare! As an example scalajs-react is mostly that, the facades aren't as safe and nice to use as typical Scala stuff, so here's a better layer on top of that, and it's the most time-consuming library to maintain that I have. Maybe I'm misunderstanding you but if you're suggesting having double the types with one always performing minor conversions to the other, I don't think that's the way to go.

armanbilge commented 3 years ago

I agree, now I see better where you're going with this! NullOr with implicit syntax like UndefOr is the way to go 👍

japgolly commented 3 years ago

Although you make it good point, it's an implicit conversion to add syntax rather than an implicit conversion to another type. Important distinction, my bad!

No no you're right to highlight that! I think the former is much more acceptable than the latter, and we shouldn't rule out any kind of implicit. Thanks for clarifying. So like I said I haven't put any thought into the solution yet but no implicit would be better than syntax via implicits, which would be better than implicit type conversion.

japgolly commented 3 years ago

So is js.UndefOr syntax provided simply be implicits? I hadn't really thought about it before but it's surprising. I'd bet that they aren't implicitly in-scope, I'm curious to try and use one with out import scalajs.js._. Whatever's going on there is very good prior-art so we should definitely look into it. I might be able to come up with some hacks to get implicit syntax, into implicit scope (so no import required) if that where this is going.....

armanbilge commented 3 years ago

I'd bet that they aren't implicitly in-scope,

They are :P see here https://github.com/scala-js/scala-js/blob/8a0655b2ddf3047c13d49814c4debb7f10b88931/library/src/main/scala/scala/scalajs/js/Union.scala#L108

armanbilge commented 3 years ago

Which sadly means for us, they cannot be, because we do not have access to the Union ~package~ companion object.

japgolly commented 3 years ago

Oh! That's cheating!! In the union companion object!

japgolly commented 3 years ago

Alright we'll figure something out. There are always ways hehehe. We should make our own companion object!

armanbilge commented 3 years ago

We should make our own companion object!

I think you are right, this would work!

japgolly commented 3 years ago

eg idea:

sealed trait NullOr[+A] extends js.Any
object NullOr { magic here }
armanbilge commented 3 years ago

I think it could also be type NullOr[+A] = A | Null, and the companion object trick still works, pretty sure cats does this.

japgolly commented 3 years ago

I feel like I've tried that before and it didn't work but happy to try again.

japgolly commented 3 years ago
package org.scalajs.dom

import scala.scalajs.js
import scala.scalajs.js.|
import scala.annotation.unchecked.uncheckedVariance

sealed trait NullOr1[+A] extends js.Any
object NullOr1 {
  @inline implicit final class Ops[A](private val self: NullOr1[A]) extends AnyVal {
    @inline def asUnion: A | Null = self.asInstanceOf[A | Null]
    def toOption: Option[A] = if (self eq null) None else Some(self.asInstanceOf[A])
  }
}

object ModuleNullOr2 {
  type NullOr2[+A] = (A @uncheckedVariance) | Null
  object NullOr2 {
    @inline implicit final class NullOr2Ops[A](private val self: NullOr2[A]) extends AnyVal {
      def toOption: Option[A] = if (self eq null) None else Some(self.asInstanceOf[A])
    }
  }
}
package external

trait NullOr1Test {
  import org.scalajs.dom.NullOr1

  def x1: NullOr1[Int]
  def y1 = x1.toOption
}

trait NullOr2Test {
  import org.scalajs.dom.ModuleNullOr2._

  def x2: NullOr2[Int]
  def y2 = x2.toOption
}

objects aren't implicit scopes for type aliases sadly:

[error] value toOption is not a member of org.scalajs.dom.ModuleNullOr2.NullOr2[Int]
[error]   def y2 = x2.toOption
[error]               ^
japgolly commented 3 years ago

My understanding is that from a compiler pov, a type alias should be identical to it's value, which means for every known type, the compiler would have to be aware of every equivalent type alias on the classpath and check all of their companion objects. Considering you typically add to the classpath in chunks (eg. module A depends on module B) you could have code compile in one scope and then suddenly break downstream as a new type alias companion object gets globally added to the type's scope.

armanbilge commented 3 years ago

You are absolutely right, this was marked wontfix in https://github.com/scala/bug/issues/9770. I was confusing it with an another issue that I read about in a comment in cats.

japgolly commented 3 years ago

Oh yeah, that other one's frustrated me many times

sjrd commented 3 years ago

I strongly advise against introducing anything like NullOr. The reason is that whatever you manage to do for Scala 2 will immediately break in Scala 3.

In Scala 3, | is a true union type. As long as we have nullable reference types by default in the language (i.e., no explicit nulls), any T | Null where T is a reference type will collapse at the type system level to T. This means that you will immediately lose any API that you have built for NullOr.

You could sidestep this issue with a sealed trait (or opaque type in Scala 3), combined with more implicit conversions. But if you do that you'll end up with the same problems that js.UndefOr had in 0.6.x, i.e., that it doesn't compose.

This is why there is no js.NullOr in the standard library.

If Scala 3 ever gets explicit nulls by default, we will be able to address those issues. In the meantime, we have to use T for something that is T or null in JS types.

Feel like it's ugly? Well, it's exactly the same for Java APIs that return nullable types. There's simply nothing we can do about it.

japgolly commented 3 years ago

You could sidestep this issue with a sealed trait (or opaque type in Scala 3), combined with more implicit conversions. But if you do that you'll end up with the same problems that js.UndefOr had in 0.6.x, i.e., that it doesn't compose.

Yeah sealed trait is exactly where I was going but with implicit ops, not implicit conversions. What do you mean about it not composing? Composing with what?

Feel like it's ugly? Well, it's exactly the same for Java APIs that return nullable types.

Java APIs are much better known that random pieces of JS all over the place.

There's simply nothing we can do about it.

Are you trying to motivate me to solve this problem? If not, that's the worst thing you could ever say to me. There's always something you can do about something. It just depends on resources and tradeoffs.

sjrd commented 3 years ago

Yeah sealed trait is exactly where I was going but with implicit ops, not implicit conversions. What do you mean about it not composing? Composing with what?

It doesn't compose with other union types, with type inference, with parameter type inference for functions, etc. In a sense it all boils down to type inference.

Java APIs are much better known that random pieces of JS all over the place.

That's unfair on the JS ecosystem. I would say that DOM APIs, documented on MDN, are much better documented and "known" than random pieces of Java libraries all over the place.

There's simply nothing we can do about it.

Are you trying to motivate me to solve this problem? If not, that's the worst thing you could ever say to me. There's always something you can do about something. It just depends on resources and tradeoffs.

If you actually solve it, in a way that provides good type inference and allows composability with other union types, in Scala 2 and in Scala 3, then all the better. I'm just saying that I didn't find a way so far, otherwise we would have put it in the standard library.

But my advice remains: a lot of time has been lost on this issue already, it's probably better to stick with the status quo, until the true solution naturally comes with explicit nullable types in some future Scala 3 version.

lihaoyi commented 3 years ago

My opinion here is that we should leave the status quo as is; yes it's not as strict as I would like, but the everything-is-nullable sloppiness in Scala matches reasonably well with the everything-is-nullable sloppiness in Javascript. scala-js-dom is meant to expose JS APIs using standard Scala constructs; it's not meant to come up with our own bespoke abstractions on top of the DOM, no matter how imperfect it is.

When Scala 3's across-the-board improvements to Scala's null handling becomes ubiquitous, we can see how to use that, but until then it doesn't make sense to come up with out own thing only to have to get rid of it when we adopt Scala 3s null handling

japgolly commented 3 years ago

it doesn't make sense to come up with out own thing only to have to get rid of it when we adopt Scala 3s null handling

This is @sjrd 's prediction, not mine.

If you actually solve it, in a way that provides good type inference and allows composability with other union types, in Scala 2 and in Scala 3, then all the better.

The idea in my head seems to do just this, without being a problem for Scala 3. I'll sketch it up in the coming days when I get a chance and you can see what you think. That will at least give us something concrete to debate rather than all of these hypertheticals.

And the record, I don't agree with this whole "let's do nothing, let's not make any improvements because one day: Scala 3" line of thinking. Who cares what Scala 3 does? We're gonna be cross-compiling for years and years to come, and we're gonna be bound by Scala 2 for a long time yet. I don't see the logic in not improving the status quo because one day things will change. A bit of migration in a few years (literally the worse case as I see it, it seems pretty easy to even avoid this) is a completely acceptable trade-off for years of improvement. Seriously I don't get all the fear.

oyvindberg commented 3 years ago

I started some research into working more conveniently with T | undefined | null, T | undefined and T | null some time ago, see https://github.com/ScalablyTyped/Runtime/pull/1 . I left it because it was quite unknown at the time how the new union types would work

armanbilge commented 3 years ago

@oyvindberg you do use T | Null in ST's generated facades though, correct? How/why did you decide on that?

oyvindberg commented 3 years ago

Yes, the three cases are translated into js.UndefOr[T | Null], js.UndefOr[T] and T | Null, respectively. I just wanted to see how Scala 3 with strict nulls would play out before doing anything more with it.

armanbilge commented 3 years ago

Feel like it's ugly? Well, it's exactly the same for Java APIs that return nullable types. There's simply nothing we can do about it.

Well, what is often done about this, is a Scala wrapper around the ugly Java API that injects Option in all the right places. Like, I hope we all might agree this is the idiomatic thing to do, although whether it is the right thing to do for scala-js-dom is of course a different matter. Which leads to my second question...

scala-js-dom is meant to expose JS APIs using standard Scala constructs; it's not meant to come up with our own bespoke abstractions on top of the DOM, no matter how imperfect it is.

Forgive me if I'm a broken record, but what is the goal of this library? If it's simply to provide a raw facade, why don't we just auto-generate it? Presumably because of some clunkiness with an auto-generated facade, but given the Null problem clunkiness in this situation seems inevitable? This is a genuine question: it's obvious that manually maintaining a facade for something as vast as DOM will be a never-ending job, so I'm curious what we are buying/what the trade-off is by doing this manually vs automatically.

There's always something you can do about something. It just depends on resources and tradeoffs.

On the other hand, if our goal is to provide a user-friendly library for DOM that uses the type system to communicate nullable types etc., why aren't we building a wrapper (as one might around a Java library) that uses Option etc.? As @japgolly suggested above, is this purely a resource constraint?

lihaoyi commented 3 years ago

@armanbilge this library was originally auto generated from typescripy definition files, though the generation script and the quality of the translation was poor enough that we continued maintaining the generated code manually after that. Note that this is from the 2014 era, long before things like ScalablyTyped appeared on the scene.

The original plan was for the raw. APIs to faithfully represent the Javascript APIs, and for the ext. APIs to be more clever/highlevel. For whatever reason, the former has succeeded while the latter has failed, and if i'm not mistaken we are phasing out the ext.* APIs going forward.

Bringing us back to the present, it's worth askinh what this library should be, regardless of the historical baggage. One strawman proposal could be as follows:

These are just my own opinions; despite authoring the library, I haven't been involved in maintenance for years, and I can now only be considered a user. It's up to you guys to decide how you would like to move forward. My only ask is that we preserve the good stuff that we have (i.e. the directly exposed DOM APIs) and not break them unnecessarily even while experimenting with new approaches

armanbilge commented 3 years ago

@lihaoyi Thank you for the history, definitely helps me to better understand and appreciate the library as it is today.

We move the raw. APIs into the dom. namespace ... We do not discard the ext.* namespace, but instead use it as an experimental scratch space where the maintainers can try more ambitious improvements

👍 I like this proposal.

We experiment with code generation, to see if we can get it to a level of quality and compatibility enough that we can stop hand maintaining the code.

I also think this is definitely worth experimenting with. Would you mind specifying some of your quality concerns, so I can get a better idea of where an auto-generation scheme needs improvement? See also https://github.com/scala-js/scala-js-dom/issues/486#issuecomment-898366292.

lihaoyi commented 3 years ago

Would you mind specifying some of your quality concerns, so I can get a better idea of where an auto-generation scheme needs improvement?

The original code generator was a pair of Scala programs, one that translated typescript definition files to Scala code, another that scraped https://developer.mozilla.org/en-US/docs/Web/API to fill in the scaladoc. Both were half-baked, and generated broken code in many cases. They were also never source controlled, and the typescript definition generator has been lost to the mists of time, although the doc scraper I managed to fish out of my archive https://gist.github.com/lihaoyi/86eba5e0956861350b5b98bbb87e6516.

Basically, it didn't work well at all. But it was a weekend project from someone new to Scala back when Scala was younger and Scala.js was still pre-release. I'm sure with infrastructure like ScalablyTyped to build upon you'd be able to get a much higher level of quality than I did, though it remains to be seen whether it'll be enough to match the current hand-maintained facades. The current ones are really pretty good, but who knows maybe @oyvindberg's code generator is just as good!.

If you can get code-gen working well, then it'll also make it trivial to code-gen wrappers, e.g. if someone wants a different encoding for nulls, we could just tweak the code-generator and re-generate. That's probably the path forward if we want to easily experiment with different encodings and wrappers, as manually wrapping the thousands of DOM APIs everytime we want to try something new is totally infeasible

oyvindberg commented 3 years ago

I have no doubt that code generation can get us very good results.

As you allude to @lihaoyi once we have the code generator with full knowledge about all types, sky is the limit for the what we can generate. For now a lot of effort has been spent on generating boilerplate to use third party react components in an effortless way, see for instance usage of the antd component library in a scalajs-react demo .

It's easy to envision code transformations which for instance could move all members of javascript types to extension methods and translate T | Null to Option[T], js.Promise[T] to Future[T], wrap everything in IO/ZIO, wrap event streams in fs2 streams or any other similar thing. A whole lot of fun experimentation can be done in this direction. I invite you all to the ScalablyTyped gitter channel to dream up some cool things there.


However, let's not go overboard here. scala-js-dom is at the top of most projects' dependency trees for pretty much any Scala.js project. One of the huge strengths of the Scala.js ecosystem is its unparalleled stability and strong compatibility story over time.

Reiterating what I said in https://github.com/scala-js/scala-js-dom/issues/486#issuecomment-898359503, I'd personally favor a conservative direction for scala-js-dom. It has very few problems now, and any interested party can voluntarily try a more experimental DOM wrapper. My two cents anyway

armanbilge commented 3 years ago

Thanks all, this was very informative for me :) I see the value in preserving what we have here while possibly augmenting with copy-pasta from ST, very similar to how this library was born too as far as I can tell.

That's probably the path forward if we want to easily experiment with different encodings and wrappers, as manually wrapping the thousands of DOM APIs everytime we want to try something new is totally infeasible

@japgolly I do agree with this, that's definitely a tricky thing with your NullOr proposal as it would be a lot of work to go through the source and find all the places we need to make this change. At the very least, a 2.x issue (this issue is currently in 1.x milestone).

japgolly commented 3 years ago

I'm trying to stay away from computers this weekend but let me just quickly say: this is a community project, and community is not only valuable on its own right, but also increases net value over time. Just cos I'm a maintainer now doesn't mean it's my way or the highway; if a fair representation of the community isn't onboard with something we should probably drop it. I still want to push for this because I believe it's in my, and everyone's best interests, and I still want to sketch up my ideas and have a more concrete debate, but at the end of the day if it's not the way to go then we won't. The process is a bit time consuming but I believe that properly considering ambitious ideas is the best way to operate, even if they don't end up being implemented. Null handling and code generation are two areas that could be of great benefit after an initial cost has been paid. Should we open a new issue to discuss code gen? As I see it is orthogonal to null handling.

On Fri, 13 Aug 2021, 11:34 pm Arman Bilge, @.***> wrote:

Thanks all, this was very informative for me :) I see the value in preserving what we have here while possibly augmenting with copy-pasta from ST, very similar to how this library was born too as far as I can tell.

That's probably the path forward if we want to easily experiment with different encodings and wrappers, as manually wrapping the thousands of DOM APIs everytime we want to try something new is totally infeasible

@japgolly https://github.com/japgolly I do agree with this, that's definitely a tricky thing with your NullOr proposal as it would be a lot of work to go through the source and find all the places we need to make this change. At the very least, a 2.x issue (this issue is currently in 1.x milestone).

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/scala-js/scala-js-dom/issues/481#issuecomment-898461457, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABRRN2QH3HLJLTSEU5IZ6DT4UNMVANCNFSM5CCP4QWQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&utm_campaign=notification-email .

armanbilge commented 3 years ago

I believe that properly considering ambitious ideas is the best way to operate

💯 thrilled to be working with someone who has this philosophy 😁

I still want to push for this because I believe it's in my, and everyone's best interests, and I still want to sketch up my ideas and have a more concrete debate

:+1: but imho we shouldn't put this in a 1.2 release but definitely consider it for 2.0.

Should we open a new issue to discuss code gen?

I will do this.

armanbilge commented 3 years ago

Instead of an issue, I made "discussion" https://github.com/scala-js/scala-js-dom/discussions/487 for code gen, hopefully the discussion format will work better for this topic!

japgolly commented 3 years ago

Here's a quick sketch of my proposal:

import scala.scalajs.js
import scala.scalajs.js.|

sealed trait NullOr[+A] extends js.Any

object NullOr {
  @inline def apply[A](a: A): NullOr[A] =
    a.asInstanceOf[NullOr[A]]

  @inline def empty: NullOr[Nothing] =
    null

  // Non-commutative. Do properly later.
  @inline def fromUnion[A](a: A | Null): NullOr[A] =
    a.asInstanceOf[NullOr[A]]

  @inline implicit final class Ops[A](private val self: NullOr[A]) extends AnyVal {

    @inline def asUnion: A | Null =
      self.asInstanceOf[A | Null]

    @inline final def get: A =
      self.asInstanceOf[A]

    @inline final def toOption: Option[A] =
      if (self eq null) None else Some(get)

    // ++ all the same methods as exists in js.UndefOrOps
  }
}

and some example usage

@js.native
object SomeFacade extends js.Object {
  val blah: NullOr[Int] = js.native
}

def usageExample() = {
  SomeFacade.blah.toOption: Option[Int]
  SomeFacade.blah.asUnion : Int | Null
  SomeFacade.blah.get     : Int
  SomeFacade.blah.orNull  : Integer
}

I think this is great

@sjrd @lihaoyi does seeing this clarify my original intent? Would you still have concerns with this kind of solution?

armanbilge commented 3 years ago

This proposal looks straightforward to me! Curious to hear @sjrd and @lihaoyi's thoughts.

It doesn't compose with other union types, with type inference, with parameter type inference for functions, etc. In a sense it all boils down to type inference.

I think this is still true ... I'm just not sure why it's so important, that we can't/shouldn't have NullOr at all 🤔

lihaoyi commented 3 years ago

@japgolly I think it clarifies the original intent, but I don't think it really alleviates my concerns.

Really, it comes down to not wanting the "raw" API for scala-js-dom to have anything that's not already widely used and ubiquitous. Nulls, for better or worse, are widely used in both Javascript and JVM and ubiquitous to both platforms. There's a time and place for experimenting with new encodings and new core data types, and I don't think it should be in the most widely depended on Scala.js package in the entire ecosystem.

If NullOr was already in broad usage, part of the standard library like UnderOr is, a common idiom for other Scala.js code, or commonly used for other facades e.g. on ScalablyTyped, then I'd be more willing to use it here. But none of these is currently the case.

As I said above, I think the path for trying novel ways to improve things with significant breakage (and source incompatibility with every single null-returning DOM API is a very significant breakage!) is to play around in the .ext package, possibly using codegen to ease on the maintainability, and have people organically adopt the new-and-improved way of doing things as it stabilizes and proves its worth. That seems like a much better path than rolling out this kind of experimental improvements in-place in a codebase that has traditionally been stable and widely depended upon