Search code examples
polymorphismocamlreasonbucklescriptvalue-restriction

How can I curry a function with optional parameters that generates Js.t objects in ReasionML/BuckleScript?


I have the following function

[@bs.obj]
external route:
  (
    ~_method: string,
    ~path: string,
    ~action: list(string) => unit,
    ~options: Js.t({..})=?,
    unit
  ) =>
  _ =
  "";

Since functions can be partially applied, I expect to be able to do this:

let get = route(~_method="GET");

but it gives me this error:

This expression's type contains type variables that can't be generalized:                                                
(~path: string, ~action: list(string) => unit, ~options: {_.. }=?,                                                       
unit) =>
{. "_method": string, "action": list(string) => unit,
  "options": Js.undefined({.. }), "path": string}

What am I doing wrong here?


Solution

  • This is not actually about the optional parameters and currying, but about value restriction and non-generalized, aka weak, type variables. TL;DR; either turn get into a syntactic function by adding a parameter, e.g., let get () = route(~_method="GET") ();, or create a *.rei interface file for your module.

    Longer Story

    The .. row variable denotes a polymorphic type that the compiler couldn't reduce to a normal monomorphic type (since there is evidently no usage of this function) nor it can trust that the partial application route(~_method="GET") has not actually already accessed the options parameter and may be stored somewhere in it, which should define the type.

    Therefore, the compiler, can't leave it as a polymorphic variable, nor can it give a concrete type, as a result, it produces a weak type variable, which could be seen as a reference cell for the future defined concrete type. Like an uninitialized type. It will be initialized later, by the code that uses the get function. If, at the end of the day, the type is never used, it may escape the scope of the module, which is forbidden by the OCaml/Reason typing rules. Therefore, you shall either give it a monotype manually (i.e., constraint it to some monomorphic type), or create an interface file where this value is hidden (i.e., not present), and therefore couldn't leak the scope of the module. Basically, just creating an empty .mli/.rei file with the same name as your .ml/.re file will resolve this issue. Another common solution is to turn get into a syntactic function, i.e., something with syntactically explicit variables, e.g.,

    let get () = route(~_method="GET") ();
    

    Further Reading