Когда мы должны использовать FSharpFunc.Adapt?

Глядя на источник в FSharp.Core и PowerPack, я вижу, что многие функции более высокого порядка, которые принимают функцию с двумя или более параметрами, используют FSharpFunc.Adapt. Например:

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

Документация на FSharpFunc.Adapt довольно тонкая. Является ли это общей передовой практикой, которую мы должны использовать в любое время, когда у нас есть функция более высокого порядка с аналогичной сигнатурой? Только если вызываемая функция вызывается несколько раз? Насколько оптимизируется это? Должны ли мы использовать Adapt везде, где мы можем, или только редко?

Спасибо за ваше время.

Ответы

Ответ 1

Это довольно интересно! У меня нет официальной информации (и я не видел этого документально нигде), но вот некоторые мысли о том, как функция Adapt может работать.

Такие функции, как mapi, принимают в виде формы функцию, что означает, что тип аргумента скомпилирован примерно как FSharpFunc<int, FSharpFunc<T, R>>. Однако многие функции фактически скомпилированы непосредственно как функции двух аргументов, поэтому фактическое значение обычно будет FSharpFunc<int, T, R>, которое наследуется от FSharpFunc<int, FSharpFunc<T, R>>.

Если вы вызываете эту функцию (например, f 1 "a"), компилятор F # генерирует что-то вроде этого:

FSharpFunc<int, string>.InvokeFast<a>(f, 1, "a");

Если вы посмотрите на функцию InvokeFast с помощью Reflector, вы увидите, что она проверяет, скомпилирована ли функция как оптимизированная версия (f :? FSharpFunc<int, T, R>). Если да, то он напрямую вызывает Invoke(1, "a"), а если нет, то ему нужно сделать два вызова Invoke(1).Invoke("a").

Эта проверка выполняется каждый раз, когда вы вызываете функцию, переданную как аргумент (вероятно, быстрее выполнить проверку, а затем использовать оптимизированный вызов, потому что это более распространено).

Что делает функция Adapt, так это то, что она преобразует любую функцию в FSharpFunc<T1, T2, R> (если функция не оптимизирована, она создает для нее обертку, но в большинстве случаев это не так). Вызовы адаптированной функции будут выполняться быстрее, потому что им не нужно выполнять динамическую проверку каждый раз (проверка выполняется только один раз внутри Adapt).

Итак, резюме состоит в том, что Adapt может повысить производительность, если вы вызываете функцию, переданную как аргумент, который принимает более одного аргумента много раз. Как и в случае с любыми оптимизациями, я не буду использовать это вслепую, но это интересно, когда нужно настраивать производительность!

(BTW: Спасибо за очень интересный вопрос, я не знал, что компилятор делает это: -))