Implicit Action/Func conversion captures extra expressions
Repro steps
let x (f: System.Action<int>) =
f.Invoke 99
f.Invoke 98
printfn "here"
let y () =
printfn "one time"
fun num -> printfn "%d" num
x (y ())
Expected behavior
Prints
one time
99
98
here
which is what would've been printed with (f: int -> unit) instead of Action<int>
Actual behavior
Prints
one time
99
one time
98
here
because x (y ()) is unexpectedly(?) expanded into x (Action<int> (fun num -> y () num)).
Known workarounds
Bind y () first, manually wrap fun num ... in an Action, etc.
Related information
- SDK 10.0.100-rc.1.25451.107
I think this is technically "by design" — https://github.com/fsharp/fslang-suggestions/issues/1083#issuecomment-933546500 — although it is admittedly unintuitive.
As an aside, it should be noted that delegate construction always delays (and reevaluates) the relevant function on each call, so, for example
new System.Action<int>(failwith "")is equivalent to
new System.Action<int>(fun arg -> (failwith "") arg)and doesn't fail on delegate construction, but rather delegate invocation.
In the spec here:
Type-directed Conversions at Member Invocations
As described in Method Application Resolution (see §), three type-directed conversions are applied at method invocations.
Conversion to Delegates
The first type-directed conversion converts anonymous function expressions and other function- valued arguments to delegate types. Given:
- A formal parameter of delegate type
D- An actual argument
fargof known typety1 -> ... -> tyn -> rty- Precisely
narguments to theInvokemethod of delegate typeDThen:
The parameter is interpreted as if it were written:
new D (fun arg1 ... argn -> farg arg1 ... argn)
and here:
Method Application Resolution
[…]
Two additional rules apply when checking arguments (see § for examples):
- If a formal parameter has delegate type
D, an actual argumentfarghas known typety1 -> ... -> tyn -> rty, and the number of arguments of the Invoke method of delegate typeDis preciselyn, interpret the formal parameter in the same way as the following:new D (fun arg1 ... argn -> farg arg1 ... argn).
Oof, terrible. Maybe we could have a warning when the argument is not a lambda or a simple value.
Ideally, the compiler would detect whether the F# function being converted was a closure and only emit a direct delegate when it is not