Skip to content
This repository was archived by the owner on Nov 1, 2021. It is now read-only.

Conversation

@glennsl
Copy link
Member

@glennsl glennsl commented Feb 24, 2017

This depends on #15, which isn't strictly necessary, but still very nice to have due to lack of "multiple inheritance"

The relveant commit is this: 025c25f

And the real magic is here: https://github.com/BuckleTypes/reason-js/pull/23/files#diff-f21fba065742f68ab32c1fe347b823b9R14

So what does this do?

In short, it makes e.g. element a subtype of node. Well, technically a subtype of node_like, of which node is also a subtype. But this makes it possible for a function to accept all kinds of node_likes, element, node, document whatever, as long as its type is constructed accordingly, which means Element.asNode and such are now obsolete.

So something like this:

let div = document |> Document.createElement "div";
let span = document |> Document.createElement "span";

div |> Element.appendChild (span |> Element.asNode);

Becomes simply:

let div = document |> Document.createElement "div";
let span = document |> Document.createElement "span";

div |> Element.appendChild span;

Which might not look like much, but situations like this comes up surprisingly often.

But how?

By the power of chained phantom types!

So a phantom type is a type (or maybe type parameter, instructions have been unclear) which isn't used for anything other than to differentiate between different kinds of a type. E.g.:

type a;
type b;
type thing 'a;

let iLoveAs : thing a => unit = ...
let iLoveBs : thing b => unit = ...
let iLoveEveryone : thing 'a => unit = ...

To implement subtyping, we need a "tag" for each kind of object we have, and for both technical (type generalization) and convenience reasons, we will separate the concept of inheritability from the concrete types. So if we want to specify element as a subtype of node, we would tneed a node_tag and an element_tag, as well as a node_like 'a. We can then define node and element in terms of these:

type node_tag;
type node_like 'a;
type node = node_like node_tag;
type element_tag;
type element = node_like element_tag;

That's one step, and to also make element inheritable, we just add a type parameter to element_tag and an element_like 'a type and chain them:

type node_like 'a;
type node = node_like unit;
type element_tag 'a;
type element_like 'a = node_like (element_tag 'a);
type element = element_like unit;
type htmlElement_tag;
type htmlElement = element_like htmlElement_tag;

In this example I've also used unit as a kind of short-hand terminator to avoid having to define yet more types just for that.

Awesome, but there are downsides, right?

No! Well, maybe, depending on how much you love "informative type errors". There's also some limitations:

  1. "Multiple inheritance" (for lack of a better word) is not supported (Although I think it's technically possible, the ordered nature of type parameters would make it very messy). Lots of dom objects are implement multiple interfaces, e.g. Elementand Document implement both Node and EventTarget. So one has to be picked as the "super-type", in both these cases Node is a natural pick, and EventTarget will have to be converted to. With implementation inheritance this is fortunately less of an issue.

  2. Type errors are more verbose. E.g.:

Error: This expression has type
         DomRe.htmlElement =
           unit DomRe.htmlElement_tag DomRe.element_tag DomRe.node_like
       but an expression was expected of type
         ReasonJs.Node.t_node = unit DomRe.node_like
       Type unit DomRe.htmlElement_tag DomRe.element_tag
       is not compatible with type unit 

They're very informative, containing all the information you need to figure it out, but might seem a bit scary due to its verbosity and formatting (which is also partly because types are printed with ML-formatting).

Other notes

  • In this specific implementation, the implementation inheritance does not "respect" subtyping. This means you can't use Node.* functions on node_likes, you'll have to use the module that's appropriate for the type you have. E.g. for element instead of Node.innerText use Element.innerText. There shouldn't be any issues with having the implementation inheritance respect subtyping, I'm just not sure it should. I'm not even sure there should be any implementation inheritance at all with subtyping.

glennsl and others added 28 commits February 9, 2017 22:34
the esy branch is the sandboxed one
This reverts commit 38de449.
We're tracking master and the output (and thus tests) get thrashed every time the version changes. See #17
* Removed Style module, added CssStyleDeclaration

* Fixed signature of a few CssStyleDeclaration functions

* Added a few ofNode and ofElement functions

* Removed Style module, added CssStyleDeclaration

* Fixed signature of a few CssStyleDeclaration functions

* Added a few ofNode and ofElement functions

* Fixed Promise.make

* Added reject argument to Promise.make

* Fleshed out Promise

* Removed a few semi-colons that were troubling webpack

* Added CSS2Properties + parentRule to CssStyleDeclaration
* Removed Style module, added CssStyleDeclaration

* Fixed signature of a few CssStyleDeclaration functions

* Added a few ofNode and ofElement functions

* Removed Style module, added CssStyleDeclaration

* Fixed signature of a few CssStyleDeclaration functions

* Added a few ofNode and ofElement functions

* Fixed Promise.make

* Added reject argument to Promise.make

* Fleshed out Promise

* Removed a few semi-colons that were troubling webpack

* Fleshed out an experimental fetch implementation
* Removed Style module, added CssStyleDeclaration

* Fixed signature of a few CssStyleDeclaration functions

* Added a few ofNode and ofElement functions

* Removed Style module, added CssStyleDeclaration

* Fixed signature of a few CssStyleDeclaration functions

* Added a few ofNode and ofElement functions

* Fixed Promise.make

* Added reject argument to Promise.make

* Fleshed out Promise

* Removed a few semi-colons that were troubling webpack

* Fleshed out an experimental fetch implementation
@glennsl
Copy link
Member Author

glennsl commented Feb 24, 2017

I also have an example mini-implementation here: https://github.com/glennsl/subtyping-experiment/blob/master/src/main.re

@chenglou chenglou merged commit 2342322 into reasonml-community:master Feb 24, 2017

let unwrapUnsafely = fun
| Some v => v
| None => assert false;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no exception?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tekknolagi good point. @glennsl can you provide a message here please? assert false should only ever be used in branches that really can't be reached. Invalid_argument is much better

Also, something something GADT lol

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3 GADTs

Copy link
Member Author

@glennsl glennsl Feb 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking of having functions like HtmlDivElement.create to avoid the casting and option, but maybe a GADT would be better.... Could then perhaps be used with getElementsByTagName and such too... I'll have to do some reading.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants