在F#中使用 `inline` 的用途

79

在 F# 中,inline 关键字似乎与我以前在 C 等语言中使用的有些不同。例如,它似乎会影响函数的类型(什么是“静态解析类型参数”?难道不是所有 F# 类型都是静态解析的吗?)

我应该在什么情况下使用 inline 函数呢?

4个回答

88

inline关键字表示函数定义应该插入到使用它的任何代码中。大多数情况下,这不会影响函数的类型。然而,在罕见的情况下,它可能会导致一个拥有更一般类型的函数,因为有些约束条件无法在.NET的编译形式中表达,但可以在内联函数时强制执行。

主要适用的情况是运算符的使用。

let add a b = a + b

将具有单态推断类型(可能是 int -> int -> int,但如果您的代码在该类型中使用此函数,则可能是类似于 float -> float -> float 的类型)。然而,通过标记此函数为内联,F# 编译器将推断出多态类型:

let inline add a b = a + b
// add has type ^a ->  ^b ->  ^c when ( ^a or  ^b) : (static member ( + ) :  ^a *  ^b ->  ^c)

在.NET编程中,没有一种方式可以在编译代码时以一流的方式对此类型约束进行编码。但是,F#编译器可以在内联函数的位置强制执行此约束,以便所有运算符使用在编译时解析。

类型参数^a^b^c是“静态解析类型参数”,这意味着参数的类型必须在使用这些参数的地方静态已知。这与常规类型参数(例如'a'b等)形成对比,在常规类型参数中,参数的含义类似于“稍后将提供某个类型,但该类型可以是任何类型”。


3
很抱歉,我才刚开始学习F#,完全不理解你的回答,请问你能否提供一些相关概念的URL链接给我参考? - knocte
2
p:'a - 要求 p 是类型 'a 的后代,例如 intobj'a when 'a :> SomeBaseType 等。另一方面,p:^a - 要求在代码的调用点上直接支持某些特性或子类型的类型 p,或通过打开支持模块或访问扩展方法类等方式来支持。 - George
knocte >> https://learn.microsoft.com/zh-cn/dotnet/fsharp/language-reference/generics/statically-resolved-type-parameters - Peheje

55
当你需要定义一个函数,在每次使用时必须重新评估其类型时,你应该使用inline,而不是普通函数。普通函数的类型只在第一次使用时被评估(推断),然后在之后的所有地方都被视为具有静态类型的第一个推断类型签名。
在inline的情况下,函数定义实际上是泛型/多态的,而在普通(非inline)的情况下,函数是静态的(通常是隐式的)类型。
因此,如果你使用inline,以下代码将会生效:
let inline add a b = a + b

[<EntryPoint>]
let main args = 

    let one = 1
    let two = 2
    let three = add one two
    // here add has been compiled to take 2 ints and return an int

    let dog = "dog"
    let cat = "cat"
    let dogcat = add dog cat
    // here add has been compiled to take 2 strings and return a string

    printfn "%i" three
    printfn "%s" dogcat   

    0

将编译、构建和运行以生成以下输出:
3  
dogcat

换句话说,同一个add函数定义被用来生成既可以将两个整数相加的函数,也可以将两个字符串连接起来的函数(实际上,底层的+运算符重载也是通过内联实现的)。
而这段代码与之前的代码几乎相同,唯一的区别是add函数不再声明为内联函数:
let add a b = a + b

[<EntryPoint>]
let main args = 

    let one = 1
    let two = 2
    let three = add one two
    // here add has been compiled to take 2 ints and return an int

    let dog = "dog"
    let cat = "cat"
    let dogcat = add dog cat
    // since add was not declared inline, it cannot be recompiled
    // and so we now have a type mismatch here

    printfn "%i" three
    printfn "%s" dogcat   

    0

无法编译,出现以下错误信息:
    let dogcat = add dog cat
                     ^^^ - This expression was expected to have type int
                           but instead has type string

使用内联的一个很好的例子是在你想要定义一个高阶函数(HOF,即一个接受其他函数作为参数的函数)时。一个例子是一个通用函数,用于反转一个具有2个参数的函数的参数应用顺序。
let inline flip f x y = f y x

在这个问题的回答中,@pad提供的方法是这样做的获取数组、列表或序列的第N个元素的不同参数顺序

36

何时应该使用inline函数?

实际上,在实践中,inline关键字最有价值的应用是将高阶函数内联到调用站点,其中它们的函数参数也被内联以产生单个完全优化的代码片段。

例如,以下fold函数中的inline使其快了5倍:

  let inline fold f a (xs: _ []) =
     let mutable a = a
     for i=0 to xs.Length-1 do
        a <- f a xs.[i]
     a

请注意,这与大多数其他语言中的inline几乎没有任何相似之处。您可以在C++中使用模板元编程实现类似的效果,但是F#也可以在编译后的程序集之间进行内联,因为inline通过.NET元数据传递。


2
标准库的fold函数是否被内联了? - J Cooper
5
@J Cooper: 不是直接改变其功能参数,而是将其转换为经过优化的闭包,以帮助处理某些类型。例如,我提供的 fold 在对复数进行操作时比内置的 Array.fold 快大约3倍。 - J D
有趣。使用内置的 inline 会有什么不利之处吗? - J Cooper
3
人,快3-5倍……那可真不少!在开发性能关键的库时,你是否会使用这个内联折叠而不是标准库的折叠函数? - Stephen Swensen
1
@Stephen:是的,一直在用。我们经常使用inline,但(与直觉相反)并不是因为它的内联执行! - J D
@JonHarrop 是的,正如 kvb 回答中所指出的那样,“inline” 可以提供更有用的函数类型。这确实令人惊喜。 - Display Name

11

F#组件设计指南中只提到了一点相关内容。 我的建议(与其中所说的相符)是:

  • 不要使用inline
    • 例外情况:当您编写供其他F#代码使用的数学库,并且希望编写针对不同数字数据类型通用的函数时,可以考虑使用inline

还有许多其他 "有趣" 的用途,可以使用内联和静态成员约束来执行类似于C++模板的"鸭子类型"场景。 我的建议是像瘟疫一样避免所有这些东西。

@kvb的答案更深入地介绍了什么是“静态类型约束”。


8
inline 关键字在数学库之外也非常有用。例如,在数据结构的上下文中,可以使用它将具体数据结构从抽象数据结构组合而成,而无需产生任何运行时性能损失。 - J D
1
我该如何在没有inline的情况下实现类型类? - Luiz Felipe
这个建议可能已经过时了。inline的效果很好,SRTP也得到了改进以提高可用性。它们肯定更先进,但我认为现在避免使用它们已经不是一个好主意了。 - VoronoiPotato

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接