【问题标题】:Handling "The resulting type would be infinite when unifying"处理“统一时生成的类型将是无限的”
【发布时间】:2018-10-11 20:52:16
【问题描述】:

let rec bind x f = f x |> bind bind "Hello World" (fun x -> x.toUpper()) printf "%s"

上面的代码sn-p导致这个错误“统一时生成的类型将是无限的”。

编译器的建议:“根据此程序点之前的信息查找不确定类型的对象。在此程序点之前可能需要类型注释来限制对象的类型。这可能允许解析查找。”

正如编译器所说,添加类型注释可以解决这个问题。你会如何写一个满足编译器的类型注解?

这可能会有所帮助。由于 Javascript 更宽容且不会抱怨,因此您可以查看将同一段代码翻译成 JS 的工作示例。

const bind = x => f => bind(f(x))

bind("Hello World")
(x => x.toUpperCase())
(console.log)

【问题讨论】:

    标签: recursion f# bind


    【解决方案1】:

    问题是您尝试定义的bind 函数没有您可以在标准F# 中表达的类型。你想要一个类型看起来像这样的函数:

    'A -> ('A -> 'B) -> ('B -> 'C) -> ('C -> 'D) -> ('D -> 'E) -> ....
    

    换句话说,bind 函数应该接受输入和应用到它们的函数序列,但您并没有限制序列的长度。标准的 F# 类型系统没有表达这一点的方法,因此人们通常使用其他模式。在您的情况下,它将是 |> 运算符。与您的bind 相比,您必须反复写|>,但这通常没问题:

    "Hello World" 
    |> (fun x -> x.ToUpper()) 
    |> printf "%s"
    

    也就是说,我认为您的示例选择不当,我只想写:

    printf "%s" ("Hello world".ToUpper())
    

    最后,由于 F# 静态成员约束,可以使用重载来定义类似 bind 函数的东西。 those lines,但 |> 是一种惯用且干净的解决方案,用于执行您的示例说明的事情。

    【讨论】:

    • 打败我!不过,我会继续完成我的答案,因为我去了一些你没有做的方向。
    • @rmunn 考虑到您的答案有多详细,我并不感到惊讶几秒钟!我相信这两个都值得保留!
    • 您对bind 类型的总结为'A -> ('A -> 'B) -> ('B -> 'C) -> ('C -> 'D) -> ('D -> 'E) -> .... 非常棒,恕我直言,它比我冗长的回答更好地解释了该类型的无限性质。 (尽管我同意两者都值得保留,因为人们有不同的学习方式,因此值得用两种不同的方式来解释一些东西)。
    【解决方案2】:

    总结:你不能这样做,但是那个函数在 F# 中没有用;只需使用 |> 运算符即可。

    加长版:无法以满足 F# 编译器的方式对您描述的 bind 函数进行注释。当我将let rec bind x f = f x |> bind 粘贴到 F# Interactive 中时,出现以下错误:

    error FS0001: Type mismatch. Expecting a
        ''a -> 'b'    
    but given a
        ''a -> ('a -> 'a) -> 'b'    
    The types ''b' and '('a -> 'a) -> 'b' cannot be unified.
    

    如果我们将定义重新排列成类似于let rec bind x f = bind (f x),我们会得到一个稍微简化的类型错误:

    error FS0001: Type mismatch. Expecting a
        ''a'    
    but given a
        ''b -> 'a'    
    The types ''a' and ''b -> 'a' cannot be unified.
    

    通过一点类型提示(let bind (x : 'x) (f : 'f) = ...),我们得到'a'f -> 'a 类型不能统一的错误,所以很清楚发生了什么。 'abind 的返回类型(在没有任何泛型类型名称的情况下,F# 分配它们以 'a 开头)。现在让我们看看为什么会发生这种类型的错误。

    看起来你已经知道偏应用:任何双参数函数,当给定一个参数时,返回一个函数,该函数在计算函数体之前等待它的第二个参数。换句话说,F# 中的let f a b = ... 等价于 Javascript 中的const f = a => b => ...。这里,bind 函数,当给定单个参数 x 时,返回一个函数,该函数在评估 bind 的主体之前等待 f。这意味着当bind 被传递一个参数时,它的返回类型是'f -> 'a(其中'a 是,正如我们所说,F# 编译器任意分配给bind 的结果的名称)。

    然而,这里是类型冲突的来源:值bind (f x),正如我们已经说过的,它的值是'f -> 'a也是你的bind函数的结果。这意味着它应该具有'a 类型。因此,F# 编译器需要以'a 类型与'f -> 'a 相同类型的方式编译该函数。如果可能,则输入代数'a = 'f -> 'a,然后您可以将该等式右侧的'a 展开为'f -> 'a,这样等式就变为'a = 'f -> ('f -> 'a)。您现在可以再次扩展'a,得到'a = 'f -> ('f -> ('f -> 'a))。以此类推无限。 F# 编译器不允许无限扩展类型,因此这是不允许的。

    但正如我已经指出的(以及 Tomas Petricek 解释的),您实际上并不需要 F# 中的 bind 函数。它只是一种将函数挂钩到管道中的方法,其中一个函数的输出将被传递到下一个函数的输入(如您的 Javascript 示例所示)。在 F# 中,执行此操作的惯用方法是使用“管道”运算符。而不是 bind "input value" f1 f2 f3(其中 f1、f2 和 f3 是适当类型的三个函数),在 F# 中您只需编写:

    "input value"
    |> f1
    |> f2
    |> f3
    

    这是正常的、惯用的 F#,几乎任何人都会理解,即使是那些不是特别熟悉函数式编程的人。所以在 F# 中不需要这个 bind 函数。

    【讨论】:

    • 你是个冠军,我喜欢你作为头像的知更鸟图片,我非常感谢你不遗余力地在 F# Interactive 中输入代码并解释类型系统原理,结合你和 Tomas Petricek 的答案将是理想的,这就是为什么我投票赞成他们,但最后我选择了 Tomas 的答案,因为它提到重载是实现绑定函数的一种可能方式。
    【解决方案3】:

    bind 的定义是一个功能悖论。这是自相矛盾的。

    let rec bind a f = f a |> bind
    //  val bind: (a:'a) -> (f:'a->'a) -> 'b
    

    查看它的最佳方法是添加一些类型注释,让我们将'a 替换为int 以使其更具体:

    let rec bind (a:int) (f:int->int) = f a |> bind
    //  val bind:(a:int) -> (f:int->int)-> 'a
    

    bind 接收一个数字和一个接受该数字并返回另一个数字的函数,但 bind 返回什么?系统不知道,因为它从来没有真正返回任何东西,它只会越来越深入地进入另一个级别的柯里化。这本身不是问题,F# 可以处理永不退出的例程,例如:

    let rec loop() = printfn "Hello" ; loop()
    //  val loop : unit -> 'a
    

    事实上,您可以使用任何类型注释 loop,F# 也可以这样做:

    let rec loop() : float = printfn "Hello" ; loop()
    //  val loop : unit -> float
    

    但如果我们对 bind 做同样的事情,矛盾就会变得明显:

    let rec bind (a:int) (f:int->int) : string = f a |> bind
    //  val bind:(a:int) -> (f:int->int)-> string
    

    错误信息说:

    Expecting a 'int -> string' but given a 'int -> (int -> int) -> string'
    

    为什么期待int -> string?因为这是类型所说的。我们知道f a 返回一个int,我们将它传递给bind,我们应该得到一个string,因为这是函数的最终结果。但是bind在只传一个参数的时候并没有返回string,而是返回了(f:int->int)-> string类型的函数,矛盾就在这里。左侧(=)表示bind 在接收 2 个参数时返回 string,但右侧表示它在接收 1 个参数时返回“字符串”。这是一个悖论,就像“我总是撒谎”这样的陈述。

    如果我们回到没有类型注释的初始定义,我们可以看到 bind 的推断结果类型是'b,这意味着任何类型,但它必须是一种特定类型,而不是很多类型,或者是一种会发生变化的类型每次通话。特别是'b不能等于('a->'a) -> 'b,因为它涉及'b,因此是一个圆形定义(或者可能是椭圆形),因此是无限循环。

    对于 Javascript,这不是问题,因为它不关心传递给函数或从函数返回什么类型。这就是使 F# 比 Javascript 更易于使用的关键特性。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-09-24
      • 1970-01-01
      • 2017-02-12
      • 1970-01-01
      • 2022-08-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多