fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

Implicit Action/Func conversion captures extra expressions

Open kerams opened this issue 2 months ago • 3 comments

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

kerams avatar Oct 01 '25 16:10 kerams

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 farg of known type ty1 -> ... -> tyn -> rty
  • Precisely n arguments to the Invoke method of delegate type D

Then:

  • 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 argument farg has known type ty1 -> ... -> tyn -> rty, and the number of arguments of the Invoke method of delegate type D is precisely n, interpret the formal parameter in the same way as the following: new D (fun arg1 ... argn -> farg arg1 ... argn).

brianrourkeboll avatar Oct 01 '25 18:10 brianrourkeboll

Oof, terrible. Maybe we could have a warning when the argument is not a lambda or a simple value.

kerams avatar Oct 01 '25 19:10 kerams

Ideally, the compiler would detect whether the F# function being converted was a closure and only emit a direct delegate when it is not

adam-c-anderson avatar Oct 07 '25 13:10 adam-c-anderson