Recommendations for module types default impl

I am currently creating my web api bindings in a small project, and I am not sure what should be the best way to solve a problem.

I want to implement the modules Element, HtmlElement and the others I need for the project, in order to have APIs over Dom.element, Dom.htmlElement and so on.

bs-webapi and rescript-webapi use the inheritance-based approach, taking advantage of the include keyword. However, the current documentation explicitly discourages this approach.

I can create a module type BaseElement with all the common functions that need to be shared on Element, HtmlElement and so on – this works perfectly because it is just a matter of external bindings. However, I also have an asHtmlElement -> option<Dom.htmlElement> raw JS implementation that I would like to share (because it is the same everywhere). One approach would be to have the declaration of the function in BaseElement and then use an include to make the implementation available in all the modules. However, this approach is discouraged as well.

What is the best way to create this kind of abstraction, avoiding code duplication if possible?

1 Like

my 2 cents:
If this is one time thing, it seems like so given dom API is a standard. Duplication is more recommended instead of abstraction.

1 Like

I don’t completely agree on this: duplicating code to avoid using include does not make much sense, because it is exactly what this keyword is supposed to do. If include is discouraged I imagine that manually including the same code over and over is a bad practice as well.

I would be happy to find an alternative to using include, but if the only other way is copy-pasting code, then I will continue to use include until a better way is available.

I can expand a little bit why my suggestions make sense.

Your user will appreciate you for your tedious work. If you ever read the base library, it is just headache. include is used wild and to look at the definition of List.map, it typically needs the editor to jump across several files to find its real definition if you are lucky.

Let me talk a bit about include itself.

  • It is not zero cost
  • It is magic, include M, you have no idea what M provides. And if some day, M adds a new function, it may break your code.

There is always a tension between author happiness and user happiness. In general, we favor user happiness if that’s the trade off we have to make. Of course, the ideal goal is to make both happy!

1 Like

Thanks for your reply, really appreciated!

At this point I would ask a possible alternative: could be possible in the future to provide a default implementation inside an abstract module type? I mean something like default trait implementation in Rust, even if I totally understand that the type system is quite different in Reason.

Let me explain the idea of what, in my opinion, it could happen under the hood. With the current language feature, I would include all the function declarations inside a type module, let’s call it AType (TBH, I am not sure about the naming conventions for type modules). Then I would write the default implementation as private generic free functions, for instance let’s say that AType declares a function foo, and I write foo_default as the default impl. At this point I would just write the following:

module A: AType = {
    /* ... */
    let foo = foo_default
}

In this case having a module ADefault containing the definition for the default implementation and then using include ADefault would reduce the boilerplate. However, it would also make impossible to specify a different, specialized version of the functions.

In theory, it seems reasonable to let the compiler to the work, accepting a default implementation and make everything work how I would do by hand. Don’t get me wrong: I know that there are many other things in the TODO list (and I really love the idea of having native Unicode support soon :heart:), but I also think that a real alternative to include should be taken into account.
If we are able to provide a real alternative to all the cases in which the keyword is used, we could also consider to deprecate it.

Given what you said, include can be a subtle footgun, and we all know that discouraging users is not enough to avoid the feature being used. Maybe my hypothetical solution has downsides that I cannot see at the moment, but my personal opinion is that if there’s something in the language that brings more hassle than advantages, we should at least try to fix the problem. :blush:

1 Like

I agree it’s kind of a pain to have to jump to multiple files to get some info about a function, so when I use include in my code, I usually just use it for the implementation but copy paste the interface so it is actually transparent to the user.

1 Like

I’m not sure to follow you, can’t you just include a module and shadow the functions you want to “specialize”?

Uh, to be honest I did not try, mainly because I have never developed anything particularly fine-grained in Rescript to need this sort of specialization. I just assumed that shadowing does not work in module scope, but at this point I think I am wrong about it :grin:.

In this case it would be just a matter of the behaviour of include compared to the “default implementation” idea.