Extending a JavaScript class

interop

#1

From time to time we need to interop with libraries that have JavaScript classes that we need to extend. I think this trend will grow as ES2015 and TypeScript become more widespread.

Currently, I don’t have a better way to do this interop than extending the class in a [%%bs.raw ...] block and binding to the subclass with BuckleScript bindings immediately below that. E.g., https://stackoverflow.com/q/46454098/20371 .

However, since ES2015 inheritance is a syntax sugar for prototypal inheritance and is likely to stay that way, it should be possible to write inheriting classes by binding to the right sequence of calls for prototypal inheritance. For example, if I have a class:

class Animal {
  speak() {}
}

We should be able to get BuckleScript to output something like this:

var Dog = Object.create(Animal);

Dog.prototype.constructor = function(name) {
  this.name = name;
};

Dog.prototype.speak = function() {
  console.log(this.name + " says woof!!");
};

Ideally, usage would look something like this:

module Dog = Class.Extend({
  type t;

  let supertype = "Animal";
  let make(name) = Class.Instance.make({
    "name": name
  });
  let speak(t) = Class.Instance.method(instance =>
    Js.log2(instance##name, "says woof!!");
  );
});

I’m probably missing a lot of corner cases here :slight_smile: does anybody have anything else they’re doing to extend classes?


#2

What about this perspective?

  • Extending existing ES classes is an ES concern - solve it in the ES space in a .js file.
  • When you’re done write the necessary bs/re interop module.

I think the notion of “ES2030” is more about moving forward rather than being held back by existing (class centric) JS/ES practices and conventions. And the reality of interop is that you better be comfortable with ES/JS even if you prefer to simply transpile to it.


#3

Hi Perry, extending classes in JS files or in [%%bs.raw ...] sections can certainly be done, but the way I see it, classes are not going away from JavaScript. In fact with the new syntax making them easier, they’re only going to become more prevalent in the world of ES2015+/TypeScript. My thought is that we might be able to drive extension-by-inheritance code from the BuckleScript side and give devs a nicer experience than having to fall back to JS. I’ll play around with the idea a bit more. It might well turn out that the status quo is the nicer experience after all.


#4

Thank you for bringing this up. This feature is must need for Web Component. To author web components directly with ReasonML, it needs to provide classes and extension mechanism.

As mentioned here, in this StackOverflow question, classes are must for web components

class HelloWorld extends HTMLElement {
    constructor() {
        super();
        // Attach a shadow root to the element.
        let shadowRoot = this.attachShadow({mode: 'open'});
        shadowRoot.innerHTML = `<p>hello world</p>`;
    }
}

If I try to use old ES-5 style classes, then HTMLElement doesn’t work:

function HelloWorld() {

    // Doesn't work with HTMLElement
    HTMLElement.call(this);

}

Web component is a truly great mechanism to enable polyglot programming. By providing the isolation, it paves the way for ReasonML adaptation in incremental steps.


#5

Original:

<!DOCTYPE HTML>
<html>
  <!-- https://apimeister.com/2017/06/03/writing-a-hello-world-web-component.html -->
  <head>
    <meta charset="utf-8">
    <title>Hello World</title>
  </head>
  <body>
    <hello-world></hello-world>
    <script>
     class HelloWorld extends HTMLElement {
       constructor() {
         super();
         // Attach a shadow root to the element.
         let shadowRoot = this.attachShadow({mode: 'open'});
         shadowRoot.innerHTML = `<p>hello world</p>`;
       }
     }

     customElements.define('hello-world', HelloWorld);
    </script>
  </body>
</html>

Function based version:

<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello World</title>
  </head>
  <body>
    <hello-world></hello-world>
    <script>
     /* Stripped down for clarity to Chrome support
        Version 71.0.3578.98 (Official Build) (64-bit)
      */

     /* https://github.com/webcomponents/custom-elements/blob/master/src/native-shim.js */
     function _constructHTMLElement() {
       return Reflect.construct(
         HTMLElement,
         [],
         (this.constructor)
       )
     }
     _constructHTMLElement.prototype = HTMLElement.prototype
     Object.setPrototypeOf(_constructHTMLElement, HTMLElement)

     /* based on code generated by https://babeljs.io/en/repl */
     function _extends(subClass, superClass) {
       subClass.prototype = Object.create(
         superClass && superClass.prototype,
         {
           constructor: {
             value: subClass,
             enumerable: false,
             writable: true,
             configurable: true
           }
         }
       )

       if (superClass) {
         Object.setPrototypeOf(subClass, superClass)
       }
     }

     var HelloWorld = (function () {
       _extends(HelloWorld, _constructHTMLElement)

       return HelloWorld

       // ===
       function HelloWorld() {
         var _this = Object.getPrototypeOf(HelloWorld).call(this)

         var shadowRoot = _this.attachShadow({ mode: 'open' })
         shadowRoot.innerHTML = '<p>hello world</p>'
         return _this
       }

     }())

     customElements.define('hello-world', HelloWorld)
    </script>
  </body>
</html>

Essentially the native implementations of custom elements on modern browsers didn’t bother with implementing the “traditional” method - so you end up having to polyfill it …

https://github.com/manfredsteyer/ngx-build-plus/issues/5#issuecomment-403013510

Douglas Crockford:

The Web [browser] is the most hostile software engineering environment imaginable.

And they are largely still containers of by default mutable state. Part of the appeal of OCaml is “immutability by default”. The more lax you get on mutation, the more you are trading away the benefits - at which point you might as well just throw your hands up in the air and decide that TypeScript’s structural typing is “good enough” in order the ease the pain/burden of interop.


#6

I think those are orthogonal issues. In the BuckleScript world we’ll want to extend JS classes which use inheritance as a mechanism for users to ‘hook into’ or customize their functionality. In other words, we’ll want to write classes which basically don’t contain mutable state, but just hook into existing JS classes that provide some service or other. For example, https://www.apollographql.com/docs/apollo-server/features/data-sources.html#REST-Data-Source works by having you implement fetch methods in a subclass. You don’t actually use it to deal with mutable stuff.


#7

React already uses inheritance - yet ReasonReact chose composition over inheritance.

So that’s just a stopgap measure until we can extend JS classes? Or is something else at play?

Might be something worth pondering.


#8

@peerreynders That seems to address the problem with Web Components. However, other concerns are equally valid along with the idea of immutability first approach for ReasonML.


#9

What I’m cautioning against is the “I need this, so lots of other people will need this, so BuckleScript needs this” type of attitude.

Web component is a truly great mechanism to enable polyglot programming.

Polygot programming is also about using the “right tool for the job”. So if an API forces you to play the OO game (which in my book is poor API design unless it is (a single) language-level API) maybe you should be interacting directly with that API with an OO language (e.g. JavaScript or TypeScript) and build an anti-corruption layer for your functional core (functional core/imperative shell).

For example with the introduction of Hooks React is further reducing it’s coupling to OO style practices and while OCaml supports OO style programming it tends to be rarely used (RWO).

Just because ReasonML has a JavaScript style syntax don’t mistake it for some weird, functional variant of JavaScript.

BuckleScript:

Write safer and simpler code in OCaml & Reason, compile to JavaScript.

Interop:

Interop in the context of BuckleScript, means communicating with JavaScript.

So it’s not entirely clear whether extending JavaScript classes is within the scope or part of the overall vision of BuckleScript. And even if it is, it still may be a matter of priority. The appearance of these types of features should never be taken for granted no matter how “convenient” they could be. So if you feel you need a solution right now you’d be best advised to explore alternative (less convenient) options.


#10

I think there has been a communication gap. I’m not advocating for adding a JavaScript class inheritance extension point to BuckleScript; I’m asking if anyone is writing code using existing BuckleScript features to output prototypal inheritance code, as my quick sketch code showed above.

That is indeed the approach I proposed in the linked Stack Overflow answer in my first message; however, I don’t necessarily think it’s the be-all-and-end-all for working with inheritance-heavy JS APIs. For one thing, it’s quite a lot to take in for newcomers. Hence, I’m trying to find a more elegant-looking (and safer) solution.