Function that returns another function executes the body of outer function on every call of the returned function

It is actually pretty unxpected to me but consider this snippet in F#:

let f x =
  printfn $"{x}"
  fun x' -> x'

let y<'t> = f 1 //> val y<'t> : (obj -> obj)
y 2
//> 
//1
//val it: obj = 2

what I would expect is that it will print "1" only when you bind f 1 to "y" (and that would tell me that "f" body only executes once) but seem like it executes "f" body on the every call of "y". Is it unavoidable effect related to auto curring or I'm missing something and there is a way to bypass outer function body execution on the every call of the returned function?


Solution 1:

The hint as to what's going on here is the fact that 't has been constrained to obj and the signature of y is (obj -> obj). That's the F# compiler effectively say, "I give up, these have no real types, it is whatever it is" and emitting something that can execute at runtime but without any real type safety.

A side effect of this is that because it can't "pin down" y to a known signature, it cannot evaluate f, so it just emits y as a direct call to f, since you've effectively told the compiler that this is fine by parameterizing it with 't (which ends up just being obj, or "whatever").

Why is this happening? Value restriction!

I suspect you've evaluated this in F# Interactive block-by-block. The line of code that defines let y = f 1 is not possible to compile with more information. You can do so in two ways:

  1. Use y with a real type that will pin its signature to the type you're using it as.
  2. Give it an explicit signature like let y: int -> int = f 1 so that it's pinned down to a concrete type.

That's why if you execute this entire snippet in FSI or run it as a program, things work exactly like you'd expect:

let f x =
  printfn $"{x}"
  fun x' -> x'

let y = f 1

y 2
y 3

Solution 2:

This is because y is generic.

Every time you refer to y, you choose a particular 't to go with that. For example:

let a = y<int>
let b = y<string>

a and b cannot be the same value, because they have been obtained from different instantiations of y. They have to be two different values. And this in turn means that y itself cannot be a single value. It has to be a function.

And that's what it is under the hood: it's compiled as a function, and every time you refer to it, the function is instantiated with the generic parameter you chose, and the body of the function is executed to obtain the result.

If you remove the generic parameter and give y a concrete type, the issue should go away:

let y = f 1 : obj -> obj