Skip to content

Roadmap proposal for Opal v2 #2231

Open
@hmdne

Description

@hmdne

This is more of a discussion I want to start, but it more fits to be an issue than discussion, because it's more related to development (also discussions don't back-notify issues and pull requests). I could add some issues to milestones, but I want to rather start a discussion for now because I'm certainly missing some parts.

We are reaching a point of time when we (or at least some of us) deliberately want to break some old APIs, some issues are kind of urgent, some are better to be solved sooner than later. Let's call this point of time "2.0", but also we can do some interim period when we notify users that they should change their behavior and possibly backport some of those new things in a safe way. Let's call this period "1.x".

If we decide that this plan is too big for "2.0", because we want to release "2.0" soon enough, we may want to split it into two parts, one "2.0", one "3.0".

The Great Symbolization

Target: 2.0. Pull request: #2223 (previous attempt: #2144). Status: early progress.

There's quite an urgent need to hide our Ruby methods behind a JS Symbol wall. From what I know, it involves compatibility between Asciidoctor and some MongoDB integration. This integration uses "$" attributes for its own use.

We could then possibly bridge the JS Symbols to our Symbols (instead of aliasing them to Strings). The issue here is twofold - using JS symbols incurs a small performance penalty and Ruby 1.8 (1.9?) used to have a similar issue with their Symbol tables, that every time user uses a new Symbol the memory usage grows (maybe we can find some solution in WeakRefs or FinalizationRegistry? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet ). As of now, we have a similar issue with stubs, so maybe this is not such a big concern.

I don't see really a deprecation path for this going forward, but we may postpone (or abandon) the JS Symbol + Ruby Symbol bridge until somewhere later, for example "3.0". The main issue will remain as an ABI break that we may document.

Bridge JS Promises to Opal Promises

Target: 1.x. Pull request: #2220. Status: early finalization.

When Opal was first written and Promises were planned, we were in much harder times. I may be wrong about this, but maybe JS didn't have their own Promises back then - and most certainly - they weren't in use this much as they are today. They certainly weren't such a crucial part of the APIs as they are today. Async/await wasn't a thing and we rather foolishly thought about use of Generator functions to achieve something similar to Fiber.

Now we need to deal with two types of things that we try to link with a code like:

  # {Buffer} view into the blob
  #
  # If block is given it will be called with a parameter once we receive
  # the buffer. Otherwise return a {Promise} which will resolve once we
  # receive it.
  def buffer
    promise = nil
    unless block_given?
      promise = Promise.new
      block = proc { |i| promise.resolve(i) }
    end
    resblock = proc { |i| block.call(Buffer.new(i)) }
    `#@native.arrayBuffer().then(#{resblock.to_n})`
    promise
  end

Certainly it can be written better, but why not just:

def buffer
  `#@native.arrayBuffer()`
end

(I know the current code also accepts a block, but let's skip that for now).

The problem is that we won't be able to make a full compatibility between those two. The biggest issue is that Ruby promises resolve in the same tick, while JS promises are delayed. So a code like:

a = 10
Promise.value(true).then { a = 20 }
expect(a).to be 20

Simply won't work. In my opinion this isn't that big of an issue, but can break some assumptions. We also won't be able to use methods like Promise#resolved?, because JS offers no such API for inspection (I wrote some code to facilitate that for the current usage of Opal native promises, but it won't scale for JS native promises).

Please also take note that Ruby has no Promises in corelib/stdlib whatsoever.

So, if we want to have a deprecation path going forward, I would suggest the following (also this will be relevant to the further sections): current Promise becomes Promise1, while the new Promise becomes Promise2. Opal 1.x aliases Promise to Promise1, while Opal 2.0 will alias Promise to Promise2. In the meantime, using Promise would warn a developer to explicitly use either Promise1 or Promise2. We may then drop Promise1 or move to a Gem sometime after 2.0 release.

Native not feeling native enough

Target: 1.x?, Status: early planing

I view the current Native (all Native) as an afterthought. My suggestion is to revisit all this module and try to improve on it with a better plan. As above, it can become Native1, while Native2 is being worked on.

Maybe it could be a good idea to rename it. As a newcomer I couldn't understand, native to what, native to Opal or to JS?

First, Native.convert, Native.try_convert, Native::Wrapper#to_n, which one is to be used? I may know (I don't really without reading native.rb again), but try to document it. Maybe instead of that we should try to figure out a better idea, one public interface. (We're brainstorming now, aren't we? So it may be a silly idea, but possibly we can implicitly integrate some of those conversion in the ``/#{} calls, for instance new `hello(#{world})` becomes old Native(`hello(#{world.to_n})`)? If only we could reserve a new %SOMETHING{} block... But that's not part of this proposal itself)

Similarly, Native::Wrapper#{alias_native,native_reader,native_writer,native_accessor}. In my opinion they hide quite a lot of magic. Which one is for method and which one is for properties? Well, alias suggests something related to alias so it's for methods, reader/writer/accessor is for generating getters/setters for properties, right? This is what a common sense would suggest, but that's not necessarily true:

    alias_native :offset_left, :offsetLeft
    alias_native :offset_top, :offsetTop
    alias_native :page_left, :pageLeft
    alias_native :page_top, :pageTop

Thing is, you can't even do this kind of renaming with native_{reader,writer,accessor} method.

And this example will also be a good idea to look at in a different perspective. But please bear with me for a moment, I will write another example, now rather touching Native::Object:

  widget = Native(`WidgetApp`)
  widget.toggleWidget(
    callback: proc do |pt|
      pt = Native(pt)
      lid.value = pt[:value]
    end
  )

Pardon my ugly code, but does this feel "native"? I would say, it would feel more native if it was this:

  widget = Native(`WidgetApp`)
  widget.toggle_widget(
    callback: proc do |pt|
      lid.value = pt[:value]
    end
  )

(We won't be able to make it look 100% native, since obviously we have blocks and JS land doesn't have those. Also this library isn't the highest quality out there).

The difference - autoconversion of procs and camelCase to snake_case performed transparently. Most modern JS libraries use camelCase, while it feels very unnatural for Ruby. As I noted, most, some don't, but I believe we can do something about it (Native(WidgetApp, convert_names: false) for instance).

A small thing now, @davispuh was surprised that Native::Object is a descendant of BasicObject (#2113). I understand the reason behind it, though BasicObjects are generally not so friendly. The downside is that a JS object may be likely to have a send property/method, but it can also have an each property/method, yet each is implemented. I don't view Native::Object as a pure delegation/proxy object, because it always has an alternative (hash access) for those possible methods/properties. I don't have a strong opinion here though.

OK. We are coming to the last part now. Some things in Opal are bridged, some are wrapped, some are (re)implemented. I think we can ease things out for newcomers at least a little bit. #to_n is quite a good interface in the part that we never need to think in those terms (we all know that Hash is an implementation, String is bridged, but Buffer is wrapped. opal-jquery deals with bridged objects, opal-browser with wrapped... and all those have their good reasons, but some people don't know that). So we have a thing called Kernel#Native, which basically aims to convert anything to a sort of Ruby friendly object, but it doesn't. I'll go for a moment to opal-browser land, to describe an issue.

In a web browser DOM basically consists of Nodes, that we wrap, but some nodes are Elements (like <div>text</div>), some are Text nodes (the text part in the previous div), some are other kinds of data, like <!--comments-->. Easy class structure, right? Well, then some Elements are Inputs, some are Textareas, and so on - let's ignore it though. In general, if I have a JS native DOM node, I can call DOM(node) on it to wrap it with the best opal-browser wrapping class. But for now, let's assume this, a JS function can return a DOM node, or something else, let's say a File, Blob or a Buffer - those aren't DOM nodes. Kernel#DOM expects a JS native DOM node. Native to the rescue! Except that Native won't return a value wrapped with our wrappers, but with Native::Object (even Buffers! though I'm not sure if Buffers won't be wrapped with Native::Array). So my suggestion would be to introduce an interface of this form:

class File
  include Native2::Wrapper
  wrapping `File`
end

Or something similar. We could dispatch then Native() to an existing method with similar semantics like DOM() if a DOM Node is encountered. (Thing is, it would mean that this method changes semantics depending on the libraries loaded, but Opal kind of does that with bridging... We could add another kwarg to force it to make a Native::{Object,Array}).

And even though I promised it was the last part, while writing this I also found out something else. The interface for Wrapped values is as follows: Node.new(js_native_value). This is what we are used to, but I think .new has rather a semantic of creating new things, not wrapping, and we often want to create a new JS object then wrap it. This ends up with a messy #initialize method. Maybe it would be a good idea to replace this interface with Node.wrap(js_native_value)?

The main issue we see with all this is performance (and backwards compatibility :D). For that I have quite an easy answer: we already have the JS alternative interface and %x{}/`` if we value performance over code readability.

Buffer, Buffer::View, Buffer::Array

Target: 1.x, 2.x, 3.0?, Status: idea

This is a similar argument to what we had previously. I have a suspicion that when Opal was conceived those types weren't used that much. Those are mostly corespondent to Ruby binary string, but maybe JS offers somewhat a better interface (it offers a Float view for instance, in Ruby world we are left with pack/unpack which is really ugly). My early idea would be to make them bridged, not wrapped and rethink their Ruby interface, because what we have is kind of lacking.

String as a wrapped class, not bridged? A hybrid one?

Target: 3.0?, Status: idea

I won't expand on this too much, but as an idea, a new String could correspond to a Buffer or a String (it also doesn't cancel the previous idea, we may have a separate Buffer). Also this would allow us to make them mutable. If string mutability is what we need to have, I don't think we have a much better choice anyway.

Bignum support

Target: 1.x/2.x/3.0?, Status: early implementation, Pull request: #2219

I found out at some point, that JS (at least all the browsers we support) actually has Bignums, actually named BigInt. I toyed with this thing for a little bit, one of the biggest limitation of this type is that it only supports 5 basic operations and casting, but no support for those basic operation between a regular Number and BigInt (one must be casted). Ruby used to have a separate type for Bignum, now Bignum and Fixnum are merged to become an Integer - not good for our purposes I think (but I may not know something), anyway I don't think this is such a big issue.

And after a few hours of playing with this, I was genuinely surprised how powerful Ruby (and Opal) number system is. In almost no time I made them work with Rational, Complex and (probably) the entire Math library. And this alone makes Opal corelib more powerful than JS!

This implementation gives a non-standard #to_bm method, if we were to use this thing seriously, the numbers should automatically be casted to Bignum when they overflow (and back to Number when they would fit it), but that's incuring a performance penalty. Also this PR misses compiler support, so currently you would need to do things like "2134123412341234123412341234".to_bn. Also, we should be able to somehow gracefully fail on some environments that don't support BigInt (this PR has some preliminary support for that).

I understand that's not a most requested feature, but nevertheless it could expand Opal feasibility among scientific communities and that's what we may want to have. Also, it brings us closer to upstream Ruby.

update: we need to explore the idea of using BigInt as the native class for Ruby Integer

Final words

I think that Opal is seriously underappreciated in the Ruby community. I think we can abuse this situation to correct what is wrong, though as always a lot of users won't upgrade due to their legacy code. But still I think that it's important to improve the interfaces we provide.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions