Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReasonML binding function with config having fixed string values

Lets say, that I have this function in Javascript which can generate string based on proper configuration:

function func(config) {
  // ...
}

also, let's assume, that the config variable has structure as below (all of these can be not given to function call):

{
  "color": string,  // can be: "blue", "red", "green"
  "number": int,    // can be: any number
  "other": string,  // can be: "x", "y"
}

How to create proper binding for this? I'm stuck with:

[@bs.deriving abstract]
type options = {
  [@bs.optional]
  color: [@bs.string] [ | `blue | `red | `green ]
  [@bs.optional]
  number: int,
  [@bs.optional]
  other: [@bs.string] [ | `x | `y ]
}

[@bs.module]
external func: options => string = "func";

But it does not work when trying to use like this:

let config = MyModule.config(
  ~color=`blue,
  ~number=123,
  ~other=`x
);

let value = MyModule.func(config);

The color and other values are integers, not strings.

like image 342
Jazi Avatar asked Feb 28 '26 07:02

Jazi


2 Answers

This is a case of a JavaScript idiom for named parameters (objects with optional fields), needing to be adapted to the OCaml/ReasonML idiom (functions with actual labelled parameters). You would do this in three steps. Step 1, as Glenn showed, define an external for the config:

type config;
[@bs.obj] external config: (
  ~color:[@bs.string] [`blue | `red | `green]=?,
  ~number:int=?,
  ~other:[@bs.string] [`x | `y]=?,
 unit,
) => config = "";

Step 2, bind to the JavaScript function using the JavaScript style of the config object:

[@bs.val] external func: config => string = "";

Step 3, wrap the JavaScript function binding in an OCaml-idiomatic function with labelled parameters:

let func(~color=?, ~number=?, ~other=?, ()) = ()
  |> config(~color?, ~number?, ~other?)
  |> func;

You can use it like this:

let result = func(~color=`blue, ());
like image 103
Yawar Avatar answered Mar 02 '26 14:03

Yawar


The @bs attributes are often poorly thought out hacks that you shouldn't expect to work well with other attributes, or really with anything other than exactly what the documentation explains or shows examples of. However, if an attribute is used where it is not intended you'll usually at least get a warning about the attribute being unused, which your code does.

@bs.string in particular only works on types at the outermost level of externals, i.e. on types whose values will be passed directly to the external function. There is also a way to create JavaScript objects using external functions which also happens to use less magic and give you much more control over the API. As far as I'm aware, the only downside compared to @bs.deriving is that you can't override field names using something like @bs.as. They have to be valid OCaml identifiers.

Here's your example implemented using an external function annotated with @bs.obj:

type options;
[@bs.obj] external options : (
  ~color:[@bs.string] [`blue | `red | `green]=?,
  ~number:int=?,
  ~other:[@bs.string] [`x | `y]=?,
  unit
  ) => options = "";

To use it you call it exactly as with @bs.deriving:

let config = options(~color=`blue,~number=123, ~other=`x, ());

But even with this I've encountered edge cases where integer values are passed in instead of strings. For this reason I tend to avoid the polymorphic variant attributes altogether and instead use ordinary variants along with conversion functions. This has the added benefit of being more idiomatic, blending in better and being more interoperable with non-BuckleScript code.

Here's what your example might look like using this approach:

type color = Blue | Red | Green;
let colorToString = fun
  | Blue => "blue"
  | Red => "red"
  | Green => "green";

type other = X | Y;    
let otherToString = fun
  | X => "x"
  | Y => "y";

[@bs.obj] external options : (
  ~color:string=?,
  ~number:int=?,
  ~other:string=?,
  unit
  ) => options = "";

[@bs.module] external func: options => string = "func";

let func = (~color=?, ~number=?, ~other=?, ()) =>
    func(options(
      ~color = ?Belt.Option.map(color, colorToString),
      ~number?,
      ~other = ?Belt.Option.map(other, otherToString),
      ()));

let config = func(~color=Blue,~number=123, ~other=X, ());
like image 29
glennsl Avatar answered Mar 02 '26 14:03

glennsl



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!