Looking at the source in FSharp.Core and PowerPack, I see that a lot of higher-order functions that accept a function with two or more parameters use FSharpFunc.Adapt. For example:
let mapi f (arr: ResizeArray<_>) =
let f = FSharpFunc<_,_,_>.Adapt(f)
let len = length arr
let res = new ResizeArray<_>(len)
for i = 0 to len - 1 do
res.Add(f.Invoke(i, arr.[i]))
res
The documentation on FSharpFunc.Adapt
is fairly thin. Is this a general best practice that we should be using any time we have a higher-order function with a similar signature? Only if the passed-in function is called multiple times? How much of an optimization is it? Should we be using Adapt
everywhere we can, or only rarely?
Thanks for your time.
That's quite interesting! I don't have any official information (and I didn't see this documented anywhere), but here are some thoughts on how the Adapt
function might work.
Functions like mapi
take curried form of a function, which means that the type of the argument is compiled to something like FSharpFunc<int, FSharpFunc<T, R>>
. However, many functions are actually compiled directly as functions of two arguments, so the actual value would typically be FSharpFunc<int, T, R>
which inherits from FSharpFunc<int, FSharpFunc<T, R>>
.
If you call this function (e.g. f 1 "a"
) the F# compiler generates something like this:
FSharpFunc<int, string>.InvokeFast<a>(f, 1, "a");
If you look at the InvokeFast
function using Reflector, you'll see that it tests if the function is compiled as the optimized version (f :? FSharpFunc<int, T, R>
). If yes, then it directly calls Invoke(1, "a")
and if not then it needs to make two calls Invoke(1).Invoke("a")
.
This check is done each time you call a function passed as an argument (it is probably faster to do the check and then use the optimized call, because that's more common).
What the Adapt
function does is that it converts any function to FSharpFunc<T1, T2, R>
(if the function is not optimized, it creates a wrapper for it, but that's not the case most of the time). The calls to the adapted function will be faster, because they don't need to do the dynamic check every time (the check is done only once inside Adapt
).
So, the summary is that Adapt
could improve the performance if you're calling a function passed as an argument that takes more than 1 argument a large number of times. As with any optimizations, I wouldn't use this blindly, but it is an interesting thing to be aware of when tuning the performance!
(BTW: Thanks for a very interesting question, I didn't know the compiler does this :-))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With