How to use @unwrap in the context of a binding's *callback*?

I’m wrapping a JavaScript-side object to an OCaml Object (that actually does, indeed, use dynamic-dispatch functionality; one is expected to override listener functions on the the object which are then called by the upstream library).

Currently, I have something like the following:

module SlashCommand = struct
  type context

  type t =
     < commandName : string
     ; description : string
     ; filePath : string option
     ; guildIDs : string array
     ; hasPermission :
         context -> ([`String of string | `Bool of bool][@unwrap]) [@set]
     >
     Js.t

  external create : unit -> t = "SlashCommand"
    [@@bs.new] [@@bs.module "slash-create"]
end

let default =
   let open SlashCommand in
   let command = create () in
   command##hasPermission #= (fun ctx -> `Bool false);
   command

Unfortunately, the [@unwrap] doesn’t have the desired effect in that subordinate position (i.e. when applied to the return value of a bound function, instead of applied to a plain value, or to an argument to a function bound at the top level):

// Generated by ReScript, PLEASE EDIT WITH CARE
import * as SlashCreate from "slash-create";

var command = new SlashCreate.SlashCommand({
      name: "hello",
      description: "Says hello to you."
    });

command.hasPermission = (function (x) {
    return {
            NAME: "Bool",
            VAL: false
          };
  });

I, of course, need the ‘callback’ here to directly return false (or a "string" if appropriate); the point of the [@unwrap] being to represent the callback being expected to return an untagged union of a boolean or a string value.

(I’ve run into approximately this problem quite a few times now; clearly there’s an ideal pattern/solution/idiom that I’m missing!)

unwrap works only for function parameters, not return values. For those you can use an abstract type and a classification function using runtime typechecking: https://rescript-lang.org/docs/manual/latest/api/js/types

Unfortunately, runtime behavior isn’t really an option — it’s a binding, and I’d really prefer to keep it zero-cost if at all possible.

This precise situation especially sucks, because my usual fallback of overloaded [@set] functions isn’t available when it’s buried in a callback like this … ugjfjfjvjfjf

This would not actually be ‘zero-cost’ anyway because the caller of hasPermission would need to do a runtime check for the return type; you can’t really do anything useful (safely) if you don’t know whether you’re dealing with a string or a bool. You could still make your binding zero-cost by only exposing an abstract type and telling your binding consumer to do the runtime check; but I don’t really see the point.

Actually, sorry, I misunderstood your question! Since the caller needs to construct the permission value (in the callback), it’s super simple to make this zero cost. Just use an abstract type to model the permission type, and provide two external constructors for the bool and string values.

Btw piece of advice, don’t use OO style to bind to the SlashCommand class; use an abstract type and functions with the appropriate attributes.

I generally avoid OCaml’s objects and classes; but in this particular case, I need the most direct mapping of the “dynamic dispatch receiver” behaviour to maintain the upstream API’s interaction and feels.

So, this works, and I’m surprised I didn’t think of it — but I really don’t like littering my binding with a bunch of these additional explicit-cast methods; is there nothing more idiomatic anyone can think of? ;_;

module SlashCommand = struct
  type context

  type hasPermissionReturn

  external permissionString : string -> hasPermissionReturn = "%identity"

  external permissionBool : bool -> hasPermissionReturn = "%identity"

  type t =
     < commandName : string
     ; description : string
     ; filePath : string option
     ; guildIDs : string array
     ; hasPermission : context -> hasPermissionReturn [@set] >
     Js.t

  external create : unit -> t = "SlashCommand"
    [@@bs.new] [@@bs.module "slash-create"]
end

let default =
   let open SlashCommand in
   let command = create () in
   command##hasPermission #= (fun ctx -> permissionBool false);
   command

Another, slightly-nightmarish, solution, is to simply throw away the type-information entirely. This loses a lot of type-safety, but reduces the noise involved …

module SlashCommand = struct
  type context

  (* TYPEME: Danger, Will Robinson. *)
  type unknown = Unknown : 'a -> unknown [@@unboxed]

  type t =
     < commandName : string
     ; description : string
     ; filePath : string option
     ; guildIDs : string array
     ; hasPermission : context -> unknown [@set] >
     Js.t

  external create : unit -> t = "SlashCommand"
    [@@bs.new] [@@bs.module "slash-create"]
end

let default =
   let open SlashCommand in
   let command = create () in
   command##hasPermission #= (fun ctx -> Unknown false);
   command

Side note: a record with a mutable field will produce the same JS as the object and imo is easier to work with :thinking:

module SlashCommand = struct
  type context

  (* TYPEME: Danger, Will Robinson. *)
  type unknown = Unknown : 'a -> unknown [@@unboxed]
  
  type t = {
    commandName: string;
    description: string;
    filePath: string option;
    guildIDs: string array;
    mutable hasPermission: context -> unknown;
  }

  external create : unit -> t = "SlashCommand"
    [@@bs.new] [@@bs.module "slash-create"]
end

let default =
    let open SlashCommand in
    let command = create () in
    command.hasPermission <- (fun ctx -> Unknown false);
    command
1 Like

The %identity external is the idiomatic way suggested by the ReScript docs for these dynamic JS situations: https://rescript-lang.org/docs/manual/latest/type#type-escape-hatch