Passing a function with optional parameters to another function requires type annotation?

I have a function with optional parameters and I can call it like so

let cb = (~foo: option<int>=?, ()) => {
  ignore(foo)
}

cb(~foo=3, ())

However, if I pass it to another function, “fn”, the compilation fails unless I explicitly annotate the type of “fn”

// Okay
let fn = (f: (~foo: int=?, unit) => unit) => { f(~foo=3, ()) }
fn(cb)

// Not okay
let fn = (f) => { f(~foo=3, ()) }
fn(cb)

Is it correct that if I want to pass around a function with optional parameters to other functions, it needs to be annotated in the functions where it is invoked?

Thanks in advance

1 Like

It looks like the problem is that f(~foo=3, ()) is ambiguous as to whether or not foo is optional or not, so the type checker assumes that it isn’t optional (thus the types aren’t compatible).

One solution is to use explicitly passed optional arguments.

let fn = f => {
  f(~foo=?Some(3), ())
}

The type checker will now infer the function type signature as you expect.

Playground link.

But then why would this code cb(~foo=3, ()) infer the types correctly? :thinking:

cb was already defined, so the compiler knows the type of cb when you call it directly. When it sees the expression cb(~foo=3, ()) it only has to look at the type signature of cb and check if it’s compatible. The compiler isn’t able to do this with functions that come from arguments, though. with f => f(~foo=3, ()), the compiler has no idea what type f is until you use it. If you use it in an ambiguous way, then the compiler has to make a decision for what it should be.

Note that in either case the compiler is equally “correct.” If you have expression f(~foo=3, ()), then f could either be a function with type (~foo: int, unit) => unit or type (~foo: int=?, unit) => unit. The problem is that this ambiguity may cause the compiler to make a different decision than how you expected.

These kinds of ambiguities are rare in ReScript, though, which is probably why they seem surprising when they do appear.

Thanks for the quick response. I guess that sort of defeats the purpose of using optional arguments, since you’d have to pass ~arg=?None for arguments that you want to omit. But good to know this is a possible workaround, thanks!