This is super interesting. I didn’t know that just by using an external val named default you use the default import. Very interesting, but a bit risky depending on what is your target (es6 or common js).
Your idea of bindings is very good, I never thought about that @get.
The only thing I may don’t like is that it requires a raw JS function. Ideally no function would bee needed and if so, I prefer to it not be raw, but probably it is needed to fill that gap between JS and rs.
Will it be possible to at least get rid of the need of the external chalk value? so it is not needed to use Chalk.chalk ?
Will it be possible to at least get rid of the need of the external chalk value
You can have red as @val similar to what you had initially, but in that case you will have to rename modifier functions.
So, it’s possible, even without a text()! if you define all possible variants, but I would suggest to keep variants to a minimum.
module Chalk = {
type t
@module("chalk") @val external chalk: t = "default"
@module("chalk") @val external red: t = "red"
@module("chalk") @val external bold: t = "bold"
@module("chalk") external printUnderline: string => string = "underline"
@get external addRed: t => t = "red"
@get external addBold: t => t = "bold"
@send external addPrintUnderline: (t, string) => string = "underline"
}
Js.log(Chalk.red->Chalk.addBold->Chalk.addPrintUnderline("Text"))
Js.log(Chalk.printUnderline("Text2"))
In this example we defined 3 variants styleName to start chain, addStylename to continue chain and printStylename to finalize chain. Although doable, I do not recommend that.
I see what you mean. Thank you very much for your detailed answers.
If I get it correctly, this last example mean that we will need 3 bindings for each property, like red, addRed and printRed. I understand this comes from the radically different approach that rescript and javascript has on what functions are. In JS they are regular objects and they can have properties so it allows this kind of funny additive chaining. I guess they also do some black magic on getters to build the chain of styles.
Considering all the alternatives, I think you first approach was the best one, specially knowing that it seems that Chalk doesn’t have any requirement of this context, so you can save a chain of styles on a variable and call it later without problem (not all JS libs are like that).
On top of that, it will be able to add even some more utilities like log so you can directly log it to console rather than having to pipe it.
About that low level library… I am not sure about it. It seems that it still requires some extra wrapping, and I am not up to rebuild chalk on rescript.
However while working it has the downside that it will require to generate all the possible combinations in form of bindings. This was not something I was aware when I first approached this bindings, because I didn’t realised that bold and other style modifiers are all functions and not only namespaces, so I would have to create a namespace per style with all the scoped bindings. Not a funny thing even if I automate it.
Also the usage of modules produces some useless JS code with empty objects, not sure if that can be avoided.
Suggestion for a small tweak; declaring t as a function may help further:
module Chalk = {
type t = (. string) => string
@module("chalk") @val external chalk: t = "default"
@get external red: t => t = "red"
@get external bold: t => t = "bold"
let text = (t, value) => t(. value)
}
open Chalk
Js.log(chalk->red->bold->text("Test"))
In that case code will compile fine with any function matching (. string) => string
module Chalk = {
type t = (. string) => string
@module("chalk") @val external chalk: t = "default"
@get external red: t => t = "red"
@get external bold: t => t = "bold"
let text = (t, value) => t(. value)
}
let uncurriedUpperCase = (. x) => Js.String.toUpperCase(x)
open Chalk
Js.log(uncurriedUpperCase->red->bold->text("Test")) // This is fine
What do you think about using %identity to transform t into a function type when calling text()? E.g.
module Chalk = {
type t
type fn = (. string) => string
external asFn: t => fn = "%identity"
@module("chalk") @val external chalk: t = "default"
@get external red: t => t = "red"
@get external bold: t => t = "bold"
let text = (t, value) => asFn(t)(. value)
}
Can you write an example of it?
Will the abstract type allow to get rid of the intermediary conversion? If I understand them correctly it will “hide” it to the external code, therefore anything that is not that type (even if they match) will be invalid, right?
Yes, an abstract interface type lets you keep type t = (. string) => string but not expose that fact outside the module. The downside is if you don’t duplicate your externals, they’re no longer inlined nicely.
Not sure what that means exactly. But on the examples that you posted I see that the generated output for the plain interface is terrible, with a lot of unneeded wrapper functions and wasted curry calls. Why is that? Why not duplicating the code on the interface makes this difference?