How to bind to a JS function that returns a function? (is this a compiler bug?)

Hi,

I have this code:

@module external picomatch: string => (string => bool) = "picomatch"

let main = () => {
    let isMarkdown = picomatch("**/*.md")
    isMarkdown("")
}

let _ = main()

Which compiles to:

'use strict';

var Picomatch = require("picomatch");

function main(param) {
  return Picomatch("**/*.md", "");
}

main(undefined);

exports.main = main;
/*  Not a pure module *

which obviously is wrong (the calls to picomatch and isMarkdown are combined into one call of picomatch).

It seems the compiler thinks picomatch accepts two arguments (when I uncurry picomatch, I get “This function has arity2 but was expected arity1”), but I don’t get why. So I assume, my binding is wrong.

I worked arround the problem by doing:

type picoMatcher = string => bool
@module external picomatch: string => picoMatcher = "picomatch"

But I’m still not sure if my first binding is wrong, or if the compiler is wrong.

I think the compiler should interpret string => (string => bool) as “a function that accepts a string and returns a function that accepts a string and returns bool” because the returned function is wrapped in parentheses. Am I wrong?

The issue here is that, in the type system, there is no difference between (a, b) => c, a => (b => c), and a => b => c. Those are all treated as a => b => c (a curried function) at the type level. In fact, if you use the reformatting tool, then it is converted to this:

@module external picomatch: (string, string) => bool = "picomatch"

It simply doesn’t know that the external function is not curried, and that the first argument must be applied to get back a reference to another function. So it is performing an optimization without realizing it’s not safe to do so.

So you have two options to get around this.

(1) Redefine the external binding to return an explicitly uncurried function, like this:

@module external picomatch: (string, . string) => bool = "picomatch"

The downside here is that you cannot pass this function to a higher-order function that expects an uncurried function argument. And you have to use the uncurried syntax at the call site. You can always wrap it with a normal curried function if that is a problem for you.

(2) Explicitly define the returned function’s type above the binding, and have the binding return that abstract type, like so:

type stringToBool = string => bool;

@module external picomatch: string => stringToBool = "picomatch"

let main = () => {
  let isMarkdown = picomatch("**/*.md")
  isMarkdown("")
}

This will produce the following JS output:

var Curry = require("bs-platform/lib/js/curry.js");
var Picomatch = require("picomatch");

function main(param) {
  var isMarkdown = Picomatch("**/*.md");
  return Curry._1(isMarkdown, "");
}

The downside with option #2 is that it’s not always clear what the abstract type is when you use it elsewhere, like in a separate module. In this case, inspecting the type (on-hover) will print the following:

string => stringToBool

This is fine if your type has a descriptive name indicating that it’s a callable function with specific parameter/return types, or when the function is intended to be passed to library function rather than called directly by the user. Otherwise, it can be opaque and confusing, leading the user to look at the definition to figure out how to use it. So I find it best to avoid doing this most of the time.

1 Like

Thanks for the explanation, @austindd!