使用管道或组合的类型推断失败,而正常函数调用成功

Type inference with piping or composition fails, where normal function call succeeds

我现在很少在 F# 中遇到这种困难,但是类型继承在 F# 中也不太常见,所以也许我很幸运。或者我错过了显而易见的。通常,当编译器抱怨不知道某种类型时,我会颠倒管道或组合操作数的顺序,然后就完成了。

基本上,给定函数调用作为 g(f x),它也可以作为 x |> f |> g(f >> g) x。但是今天没有...

这是我的意思的一个混乱的概念验证:

module Exc =
    open System

    type MyExc(t) = inherit Exception(t)

    let createExc t = new MyExc(t)
    type Ex = Ex of exn
    type Res = Success of string | Fail of Ex with
        static member createRes1 t = Ex(createExc(t)) |> Fail   // compiled
        static member createRes2 t =  t |> createExc |> Ex |> Fail  // FS0001
        static member createRes3 = createExc >> Ex >> Fail   // FS0001

通常情况下,这是有效的(至少根据我的经验)。带有 "fail" 的行抛出:

error FS0001: Type mismatch. Expecting a MyExc -> 'a but given a exn -> Ex. The type 'MyExc' does not match the type 'exn'

没什么大不了的,不难解决,但我碰巧不得不写很多代码,其中组合是 easier/cleaner 方法,我不想写一堆实用函数我必须到处都放。

我查看了 灵活类型 ,我猜这是一个逆变问题,但我不知道如何在此处应用它。有什么想法可以保持这种惯用语吗?

请注意,如果我重新排列,即 Ex << createExc >> Fail 或使用管道向后运算符,我最终会在不同的部分出现相同的错误。

您可以使用继承约束使类型通用。

open System

type MyExc (t) = inherit Exception (t)

let createExc t = MyExc (t)
type Ex<'t when 't :> exn> = Ex of 't
type Res<'t when 't :> exn> = Success of string | Fail of 't Ex with
  static member createRes1 t = Ex (createExc t) |> Fail
  static member createRes2 t =  t |> createExc |> Ex |> Fail
  static member createRes3 = createExc >> Ex >> Fail

在这种情况下,F# 编译器的行为有点不正常。在您的示例中,您希望将类型 MyExc 的值传递给需要 exn 的构造函数。将对象视为其基数 class 的值是一种有效的强制转换,但 F# 编译器仅在非常有限的地方插入此类转换。

特别是,它会在您将参数传递给函数时插入强制转换,但不会(例如)在创建列表或从函数返回结果时插入它们。

在您的示例中,将值传递给可区分的联合构造函数时需要进行强制转换。似乎只有在直接创建联合案例时才会发生这种情况,但在将联合案例视为函数时不会发生:

// foo is a function that takes `obj` and Foo is a DU case that takes `obj`
let foo (o:obj) = o
type Foo = Foo of obj

foo(System.Random()) // Coersion inserted automatically
Foo(System.Random()) // Coersion inserted automatically

System.Random() |> foo // Coersion inserted automatically
System.Random() |> Foo // ..but not here!

因此,F# 编译器自动应用强制转换的有限位置集包括各种调用函数的方式,但只是创建 DU 案例的直接方式。

这是一个有点有趣的行为 - 我认为将 DU 案例视为普通函数是有意义的,包括在您使用 |> 时自动插入强制转换,但我不确定是否存在是什么技术原因使这变得困难。

类型推断不适用于子类型化(其中继承是一种情况)。 H&M 算法只是没有子类型的概念,并且随着时间的推移对其进行调整的各种尝试都没有产生好的结果。 F# 编译器确实尽最大努力以特殊情况补丁的形式在可能的地方容纳子类型。例如,当实际参数是形式参数的超类型时,它会考虑函数 "compatible" 。但是由于某些原因,这个 "patch" 在将联合构造函数转换为函数时不会转换。

例如:

type U() = inherit exn()
type T = T of exn

let g f x = f x

let e = U()
let a = T e       // works
let b = g T e     // compile error: `e` was expected to have type `exn`, but here has type `U`

在最后一行,联合构造函数T被用作自由函数,因此它丢失了子类型补丁。

奇怪的是,这适用于常规函数(即那些不是作为联合构造函数开始的函数):

let makeT u = T u
let a = makeT e     // works
let b = g makeT e   // also works!

它甚至可以免费工作:

let makeT = T
let a = makeT e     // works
let b = g makeT e   // still works!

此详细信息为您提供了一个解决方法:您可以为 Ex 构造函数指定另一个名称,管道将起作用:

type Ex = Ex of exn
let makeEx = Ex

   static member createRes2 t =  t |> createExc |> makeEx |> Fail  // Should work now