【问题标题】:F# Generic methods and operatorsF# 泛型方法和运算符
【发布时间】:2012-08-10 12:10:21
【问题描述】:

到目前为止,我对 F# 中的类型推断印象深刻,但是我发现了一些它并没有真正得到的东西:

//First up a simple Vect3 type
type Vect3 = { x:float; y:float; z:float } with 
  static member (/) (v1 : Vect3, s : float) = //divide by scalar, note that float
    {x=v1.x / s; y= v1.y /s; z = v1.z /s}
  static member (-) (v1 : Vect3, v2 : Vect3) = //subtract two Vect3s
    {x=v1.x - v2.x; y= v1.y - v2.y; z=v1.z - v2.z}
  //... other operators...

//this works fine
let floatDiff h (f: float -> float) x = //returns float
  ((f (x + h)) - (f (x - h)))/(h * 2.0)

//as does this
let vectDiff h (f: float -> Vect3) x = //returns Vect3
  ((f (x + h)) - (f (x - h)))/(h * 2.0)


//I'm writing the same code twice so I try and make a generic function:
let genericDiff h (f: float -> 'a) x : 'a = //'a is constrained to a float 
  ((f (x + h)) - (f (x - h)))/(h * 2.0)

当我尝试构建最后一个函数时,一个蓝色波浪线出现在分号下方,编译器发出可怕的警告“此构造导致代码比类型注释指示的更通用。类型变量 'a 已经限制为“float”类型”。我为 Vect3 提供了适合该功能的 / 运算符。为什么要警告我?

【问题讨论】:

    标签: generics f# type-inference


    【解决方案1】:

    标准 .NET 泛型的表达能力不足以支持这种泛型函数。问题是您的代码可以适用于任何支持减法运算符的'a,但.NET 泛型无法捕获此约束(它们可以捕获接口约束,但不能捕获成员约束)。

    但是,您可以使用 F# inline 函数和 statically resolved type parameters,它们可以具有其他成员约束。我写了一个article that provides some more details 关于这些。

    简单地说,如果您将函数标记为inline 并让编译器推断类型,那么您会得到(我删除了对类型参数的明确提及,因为这会使情况变得更棘手):

    > let inline genericDiff h (f: float -> _) x = 
    >   ((f (x + h)) - (f (x - h))) / (h * 2.0);;
    
    val inline genericDiff :
      float -> (float ->  ^a) -> float -> float
        when  ^a : (static member ( - ) :  ^a *  ^a -> float)
    

    编译器现在使用^a 而不是'a 来表示参数是静态解析的(在内联期间),它添加了一个约束,说^a 必须有一个成员- 需要两个事物并返回float

    遗憾的是,这并不是您想要的,因为您的 - 运算符返回 Vect3(而不是编译器推断的 float)。我认为问题在于编译器希望 / 运算符具有两个相同类型的参数(而您的是 Vect3 * float)。您可以使用不同的运营商名称(例如,/.):

    let inline genericDiff2 h (f: float -> _) x = 
      ((f (x + h)) - (f (x - h))) /. (h * 2.0);;
    

    在这种情况下,它将适用于 Vect3(如果您重命名标量除法),但它不会轻松适用于 float(尽管可能会有一些黑客有可能——see this answer——虽然我不会考虑那种惯用的 F#,我可能会尝试找到一种方法来避免这种情况)。提供元素除法并将h 作为Vect3 值传递是否有意义?

    【讨论】:

    • 我认为浮动文字 2.0 是导致问题的原因。使用通用数字,使用/ 的函数可以工作。
    • @Daniel 有趣。我以为我尝试过类似的方法,但编译器对^T * float -> ^T 不满意。它绝对适用于通用文字(正如您在回答中所做的那样)。
    • @Tomas 感谢您提供文章的链接,我之前读过关于泛型的不同注释,但并没有真正理解它们。这篇文章确实帮助我理解了它们以及为什么它在数字代码中很有用。 @Daniel 我不明白为什么文字应该限制返回类型,编译器是否假定 / 上的 arg 类型相同?
    • 如果其中一种类型是已知的(在这种情况下为浮点数),它似乎会在该类型上寻找static member (/),而如果这两种类型都是通用的,它似乎会同时查看两者(什么你期待)。简单示例:let inline divByTwo x = x / 2.0x 约束为浮点数,因为 float/ 定义为 float -> float -> float。它以可能的唯一方式解决类型。
    • 我认为这是一个错误,如果指定了 inline ,它应该查看两种类型,如果仍然不知道一个,那么在调用站点知道这两种类型之前不要解决它。我过去遇到过这个问题,将其报告为错误。
    【解决方案2】:

    如果您对文字使用通用数字,它会起作用:

    let inline genericDiff h f x =
      let one = LanguagePrimitives.GenericOne
      let two = one + one
      ((f (x + h)) - (f (x - h))) / (h * two)
    
    genericDiff 1.0 (fun y -> {x=y; y=y; z=y}) 1.0 //{x = 1.0; y = 1.0; z = 1.0;}
    

    【讨论】:

    • 或者,将h * two 替换为h + h 以去掉两行
    • 是的。这是为了展示该问题的一般解决方案。
    • 这似乎是一个奇怪的概念,必须从很多常量中建立更大的常量:)。我想一旦你有两个,你就可以开始乘法了。所以如果常数是 19,你会这样做:let sixteen = two * two * two * two; let myConst = sixteen + two + one; 很好玩!
    • @Ciemnl 或者,您可以使用类似于此答案stackoverflow.com/questions/11562037/function-templates-in-f 中提供的代码来避免必须乘以通用数字。
    【解决方案3】:

    由于某种原因,编译器假定除法的类型签名是

    ^a*^a -> ^b
    

    什么时候应该可以

    ^a*^c -> ^b
    

    我相信spec 中的 9.7 暗示了这一点(这是用于度量单位,但我不明白为什么它们是特殊情况)。如果除法可以具有所需的类型签名,您可以这样做:

    let inline genericDiff h (f: float -> ^a) x = 
        let inline sub a b = (^a: (static member (-):^a * ^a-> ^a) (a,b))
        let inline div a b = (^a: (static member (/):^a -> float-> ^c) (a,b))
        div (sub (f (x+h)) (f(x-h))) (h*2.0)
    

    我已经包含了隐含的 subdiv,但它们不应该是必需的 - 想法是使签名明确。

    我相信这不允许^a 成为浮点数之外的任何东西这一事实实际上可能是一个错误。

    【讨论】:

      猜你喜欢
      • 2016-01-25
      • 1970-01-01
      • 2010-10-16
      • 2016-06-10
      • 2023-03-24
      • 2010-10-14
      • 2011-08-19
      • 1970-01-01
      相关资源
      最近更新 更多