Search code examples
promisereasonbucklescript

Understanding Js.Promise.resolve(. ) dot syntax in ReasonML


I'm trying to understand the docs: https://reasonml.github.io/docs/en/promise

In the usage section there is:

let myPromise = Js.Promise.make((~resolve, ~reject) => resolve(. 2));

Why is there is dot before the 2? What does it mean and what does it do?


Solution

  • (. ) as used here, in function application, means the function should be called with an uncurried calling convention.

    When used in a function type, like the type of resolve here, (. 'a) => unit, it means the function is uncurried.

    Ok, so what the hell does that mean? Wweeellll, that's a bit of a story. Here goes:

    What is uncurrying?

    Uncurrying is the opposite of currying, so let's explain that first and then compare.

    Currying is the process of transforming a function that takes multiple arguments into a series of functions that take exactly one argument each and return either the final return value or a function taking the next argument. In Reason/OCaml this is done automatically for us, and is why in OCaml function types have arrows between its arguments (e.g. 'a -> 'b -> 'ret). You can write function types in this way in Reason too ('a => 'b => 'ret), but by default it's hidden by the syntax (('a, 'b) => 'ret), which is meant well but might also make it more difficult to understand why functions behave unexpectedly in some circumstances.

    In uncurried languages supporting first-class functions you can also curry functions manually. Let's look at an example in ES6. This is a normal "uncurried" ES6 function:

    let add = (a, b) => a + b;
    

    and this is its curried form:

    let add = a => b => a + b;
    

    and with parentheses to emphasize the separate functions:

    let add = a => (b => a + b);
    

    The first function takes the argument a, then returns a function (that closes over a) which takes the argument b and then computes the final return value.

    This is cool because we can easily partially apply the a argument without using bind, but it's a bit inconvenient to apply all arguments to it at once, since we have to call each function individually:

    let result = add(2)(3);
    

    So not only does Reason/OCaml automatically curry functions upon creation, but it also provides a calling convention that lets us conveniently apply multiple arguments too.

    And this all works great! ...as long as every function is curried. But then we want to hang out and talk with JavaScript, where most functions are not (but see Ramda for one notable exception). To be able to call uncurried JavaScript functions we need an uncurried calling convention, and to be able to create functions that can be called as expected from JavaScript we need an uncurried function type.

    Why does resolve need to be uncurried?

    An even better question might be "why aren't all external functions uncurried"? The answer is that they actually are, but the type and calling convention can often both be inferred at compile-time. And if not it can often be "reflected" at runtime by inspecting the function value, at a small (but quickly compounding) cost to performance. The exceptions to this are where it gets a bit muddy, since the documentation doesn't explain exactly when explicit uncurrying is required for correct functioning, and when it isn't required but can be beneficial for performance reasons. In addition, there's actually two annotations for uncurried functions, one which can infer the calling convention and one which requires it to be explicit, as is the case here. But this is what I've gathered.

    Let's look at the full signature for Js.Promise.make, which is interesting because it includes three kinds of uncurried functions:

    [@bs.new]
    external make :
        ([@bs.uncurry] (
            (~resolve: (. 'a) => unit,
             ~reject: (. exn) => unit) => unit)) => t('a) = "Promise";
    

    Or in OCaml syntax, which I find significantly more readable in this case:

    external make : (resolve:('a -> unit [@bs]) ->
                     reject:(exn -> unit [@bs]) -> unit [@bs.uncurry]) -> 'a t = "Promise" [@@bs.new]
    

    The first kind of function is make itself, which is an external and can be inferred to be uncurried because all externals are of course implemented in JavaScript.

    The second kind of function is the callback we'll create and pass to make. This must be uncurried because it's called from JavaScript with an uncurried calling convention. But since functions we create are curried by default, [@bs.uncurry] is used here to specify both that it expects an uncurried function, and that it should be uncurried automatically.

    The third kind of function is resolve and reject, which are callback functions passed back from JavaScript and thus uncurried. But these are also 1-ary functions, where you'd think the curried and uncurried form should be exactly the same. And with ordinary monomorphic functions you'd be right, but unfortunately resolve is polymorphic, which creates some problems.

    If the return type had been polymorphic, the function might actually not be 1-ary in curried form, since the return value could itself be a function taking another argument, which could return another function and so on. That's one downside of currying. But fortunately it isn't, so we know it's 1-ary.

    I think the problem is even more subtle than that. It might arise because we need to be able to represent 0-ary uncurried functions using curried function types, which are all 1-ary. How do we do that? Well, if you were to implement an equivalent function in Reason/OCaml, you'd use unit as the argument type, so let's just do that. But now, if you have a polymorphic function, it might be 0-ary if it's monomorphized as unit and 1-ary otherwise. And I suppose calling a 0-ary function with one argument has been deemed unsound in some way.

    But why, then, does reject need to be uncurried, when it's not polymorphic?

    Well... my best guess is that it's just for consistency.


    For more info, see the manual (but note that it confuses currying with partial application)