How can improve this chalk bindings?

Hello.

I want to create some chalk bindings but the bindings syntax is a bit confusing when everything is nested under the same root module.

I want to use a submodule Bold containing all methods that bind to chalk.bold but I can’t figure it out how.
This is what I have so far:

@module("chalk") external red: string => string = "red"
@module("chalk") external blue: string => string = "blue"
@module("chalk") external green: string => string = "green"
type bold
@module("chalk") external bold: bold = "bold"
@send external bold_yellow: (bold, string) => string = "yellow"
@send external bold_cyan: (bold, string) => string = "cyan"

However this is not very ergonomic to use:

Chalk.bold->Chalk.bold_yellow("WARNING")

While my ideal usage would be

Chalk.Bold.yellow("WARNING")

Which will allow things like

open Chalk.bold
yellow("bla")

Not sure how can I model the correct @send inside the module

This produces exactly what I want, but seems brittle and error prone:

module Bold = {
  @val external cyan: string => string = "Chalk.bold.cyan"
}
1 Like

If you want to keep chaining as in original api, you can model it this way:

{
  open Chalk; 
  chalk->red->bold->text("Test")
}
module Chalk = {
  type t

  @module("chalk") @val external chalk: t = "default"

  @get external red: t => t = "red"
  @get external bold: t => t = "bold"

  let text: (t, string) => string = %raw(`function (chalk, text) { return chalk(text) }`)
}
1 Like

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.

Actually I think that it would be best to try to implement following api over underlying https://github.com/chalk/ansi-styles library.

open AnsiStyles
let _ = wrap(
  ~color=Colors.red, 
  ~background=Colors.green, 
  ~underline=true, 
  "Text to wrap"
)
1 Like

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.

Thank you.

I just found another nasty way:

module Chalk = {
  @module("chalk") external red: string => string = "red"
  module Bold = {
    @module("chalk") @scope("bold") external red: string => string = "red"
  }
}

Js.log(Chalk.Bold.red("Text"))
Js.log(Chalk.red("Text"))

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.

@vdanchenkov has great solutions here.

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"))

Which generates:

console.log(Chalk.red.bold("Test"));
3 Likes

That is awesome!
By the way, being a function of a single argument, why does it need to be uncurried?

1 Like

I’m not sure why, but this case isn’t optimised by the compiler into an uncurried function. So the uncurried function syntax is helpful here.

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

2 Likes

Type t should probably be abstract indeed.

1 Like

Great point @vdanchenkov

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)
}
1 Like

Yeah, it should work with no issues.

1 Like

I would still recommend to use an abstract type over the identity function here.

1 Like

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.

plain interface
https://rescript-lang.org/try?code=LYewJgrgNgpgBAFwJ4Ad4GEAWBDKBrASQDsEYAnAM2wGN4BeOAbwCg5FV4FW5YE5qc+AFyJuvOGRhgRfOgD5RbcQCMQUaYjjzFPGH1IAPBCIAUCADRwAzgjIBLIgHMAlFoU37T5gF9mzUJCwcFi4eCIh+MSklDT0TNzIaJoMJgB01rYOLm4Zno5+bAACAdAwJgBEAqHlroUAbrhwMEbkRI1Vwslw5WAwVNAI5QVwhY56TS1kbVASUjI5st2SYENFY3zN0dNwqurz2ovluyvD4oaLZpYNUBAwrgdpcNe3zj5+IGhEwYJ4zABSVlSUBAjhMHTwAFo5MsoccoecKgAVGA2GrOIA

duplicate externals
https://rescript-lang.org/try?code=LYewJgrgNgpgBAFwJ4Ad4GEAWBDKBrASQDsEYAnAM2wGN4BeOAbwCg5FV4FW4ABUSWAAoARNRz5hASl4A3XHBgAPUmSLyxuPAC5EcBsLAwq0BMObceAcxgIFy8mqhwyMMDtt0AfLv0uwZtisbOxVHOAAjECg3H28POGFI6LNuWFtSZR1BBAAaOABnBDIASyJLaS8CotLLZgBfc35oDHFtOCxNYhUqWj0mbmQ0HzhBADoqkrKK70LJ2osmoVFWqVl5JVD1Vvc+gyNsExTA61sNh3k-Hcr44T8A3hOQ86ckmI843dejuDTEDb7snk5FAIDBpogxnBgaDJPVzCA0ER2q1mAApfKjKAgSyCDT4AC0nj8hNehIyCBEABUYIUpJIgA

Both of these prevent code using the Chalk module from doing anything with variables of type Chalk.t except pass them to methods in the module.

1 Like

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?

When the interface doesn’t duplicate the external it is exported as a regular function (and recall that functions are curried by default).

When the external is duplicated, other modules have available the details of the special external and the compiler can optimise away the currying.