【问题标题】:Generic higher order function通用高阶函数
【发布时间】:2011-08-27 09:16:24
【问题描述】:

当我将泛型函数作为本地值传递时,我可以使用具有不同类型参数的泛型函数,但在作为参数传递时不能使用,这有什么原因吗? 例如:

let f = id

let g (x,y) = (f x, f y)

g ( 1, '2')

工作正常,但如果我尝试将函数作为参数传递

let g f (x,y) = (f x, f y)

g id ( 1, '2')

它失败了,因为它采用版本 f 并尝试应用它两次。

我找到了一种解决方法,但它迫使我写两次我传递的函数:

let g f1 f2 (x,y) = (f1 x, f2 y)

g id id ( 1, '2')

这是我第二次遇到这个问题。

为什么它会这样,如果函数是本地值或作为参数传递,它不应该是相同的?

有没有办法在不复制函数的情况下做到这一点?

一个 hack,可能使用显式类型约束、内联魔法、引号?

【问题讨论】:

    标签: generics f# inline


    【解决方案1】:

    这是内联魔法。 让我们使用 kvb 的代码并定义一个处理所有情况的 gmap 函数:

    let inline gmap f (x, y) = f $ x, f $ y
    
    type One = One with static member ($) (One, x) = 1  // Example1 ConvertAll
    type Id  = Id  with static member ($) (Id , x) = x  // Example2 PassThrough
    
    type SeqSingleton  = SeqSingleton  with static member ($) (SeqSingleton , x) = seq [x]
    type ListSingleton = ListSingleton with static member ($) (ListSingleton, x) = [x]
    type ListHead      = ListHead      with static member ($) (ListHead, x) = List.head x
    
    // Usage
    let pair1 = gmap One ("test", true)
    let pair2 = gmap Id  ("test", true)
    let pair3 = gmap SeqSingleton  ("test", true)
    let pair4 = gmap ListSingleton ("test", true)
    let pair5 = gmap ListHead (["test";"test2"], [true;false])
    
    let pair6 = ("test", true) |> gmap ListSingleton |> gmap ListHead
    
    (* results
    val pair1 : int * int = (1, 1)
    val pair2 : string * bool = ("test", true)
    val pair3 : seq<string> * seq<bool> = (["test"], [true])
    val pair4 : string list * bool list = (["test"], [true])
    val pair5 : string * bool = ("test", true)
    val pair6 : string * bool = ("test", true)
    *)
    

    更新

    还可以使用定义为here 的更通用的gmap 函数,然后它也适用于n-uples (n

    【讨论】:

    • 嘿,终于有一个很好的解决方法了。看起来内联魔法比高级类型更灵活:)。我喜欢它。非常感谢!!!
    【解决方案2】:

    正如 rkhayrov 在评论中提到的,当您可以拥有更高级别的类型时,类型推断是不可能的。在您的示例中,您有

    let g f (x,y) = (f x, f y)
    

    g 有两种可能的类型,它们不兼容(用一种混合 F#/Haskell 语法编写):

    1. 对于所有'b,'c,'d。 (((forall 'a .'a -> 'b) -> 'c * 'd -> 'b * 'b)
    2. forall 'c, 'd. (forall 'a . 'a -> 'a) -> 'c * 'd -> 'c * 'd)

    给定第一种类型,我们可以调用g (fun x -&gt; 1) ("test", true) 并获取(1,1)。给定第二种类型,我们可以调用g id ("test", true) 并得到("test", true)。两种类型都不比另一种更通用。

    如果您想在 F# 中使用排名较高的类型,可以,但您必须明确并使用中间名义类型。这是对上述每种可能性进行编码的一种方法:

    module Example1 = 
        type ConvertAll<'b> =
            abstract Invoke<'a> : 'a -> 'b
    
        let g (f:ConvertAll<'b>) (x,y) = (f.Invoke x, f.Invoke y)
    
        //usage
        let pair = g { new ConvertAll<int> with member __.Invoke(x) = 1 } ("test", true)
    
    module Example2 = 
        type PassThrough =
            abstract Invoke<'a> : 'a -> 'a
    
        let g (f:PassThrough) (x,y) = (f.Invoke x, f.Invoke y)
    
        //usage
        let pair = g { new PassThrough with member __.Invoke(x) = x } ("test", true)
    

    【讨论】:

    • 感谢您的回答。所以我需要为每种转换编写一个版本的 g 。我认为这同样适用于 Haskell 中的高级类型,对吧?
    • @Michael - 我不是 Haskell 专家,但我相信这是正确的。
    • 当你有更高级别的类型时,类型推断不一定是不可能的。它们仍然可能受到限制(例如 rank-2、删除递归类型、添加注释)。请参阅github.com/cdiggins/type-inference 以了解推断更高等级类型的算法。或 research.microsoft.com/en-us/um/people/simonpj/papers/higher-rank/putting.pdf
    • @cdiggins - 当然,这是一个好点;但是,由于您在超过排名 1 时会丢失主要类型,因此即使可以推断出一种类型,它也不一定是“正确”的类型,正如我的答案中的示例所示。
    【解决方案3】:

    我猜这是预期的行为:

    在第一种情况下,您调用两个不同版本的f(一个带有int,一个带有char),在第二种情况下,您对两者都使用相同的版本,编译器会推断出它(从上到下, left-right - 记得吗?)是int-&gt;int

    问题在于通用版本将由编译器转换为具体版本。我认为没有解决方法 - 即使inline 也没有,但也许有人可以在这里发挥一些魔法;)

    【讨论】:

    • 扩展:在第二个版本中,当它试图推断f 的类型时,它会查看你给它的参数。你给了它一个数字,所以它推断它是int -&gt; 'b。但随后它看到你给了它一个字符串,并得到一个错误,因为它无法统一intstring
    • 所以编译器在将函数作为参数传递时总是将通用版本转换为具体版本?总是这样吗?但我认为 IL 级别支持泛型。
    • 你是对的 - 泛型是 IL 级别,但编译器会检查类型 - 所以如果我把它想象成 translated,我会发现它更容易
    【解决方案4】:

    我认为内联魔法在这里也行不通,例如尝试

     let inline g f (x:^a,y:^b) = (f x,f y);;
    

    在 FSI 中失败,因为编译器推断 ^b 必须与 ^a 相同

    考虑到这一点,失败变得更明显了,因为我们把函数f应用到x上,它必须有一个像^a -&gt; 'c这样的签名,如果我们尝试将函数应用到一个元素上就会失败类型为^b

    如果您考虑这个问题,我不能定义一个接受 int-&gt;string 的函数并期望它在没有重载的情况下为 char-&gt;string 工作,并且您不能将重载作为参数传递。

    【讨论】:

    • 是的,我刚刚提到了内联,因为在第一个示例中,我将函数定义为本地值并且它可以工作,而且我知道内联获取函数并在本地重写它们。
    【解决方案5】:

    原因是当 f 作为参数传递给 g 时,编译器会尝试推断 f 的类型。基于函数 g 主体中 f 的用法,编译器推断该函数同时应用于 x 和 y,这意味着 x 和 y 参数必须是相同的类型,这就是 f 参数的类型。在您的代码示例中,您将 x 作为整数传递,这会使编译器推断 y 也将是整数类型。

    更新:

    你可以这样做:

    let g (f:obj -> obj) (x : 'a,y : 'b) = (f x :?> 'a ,f y :?> 'b)
    g id (1,"1") |> printfn "%A"
    

    函数 f 需要确保它返回的主类型与它接收到的 obj 类型相同

    【讨论】:

    • Sepcifying 类型也不起作用。例如:对于 f 你指定 'a -> 'a 然后 x 和 y 都变成 'a 即相同类型
    • 您能告诉我们您尝试使用的任何“真实代码”吗?还是只是与编译器打架:)
    • 例如,现在我正在使用泛型类型的键值对,我想定义一个函数,将函数映射到键和值,但它们可以有不同的类型和函数通过 能够处理这些类型。所以我现在正在做的是将函数传递两次,就像我的第三个示例中显示的那样,代码看起来真的很愚蠢,如果你看它,你认为的第一件事是“这可以使用一个接受值复制的函数来解决它”,但它不能。
    • @Michael 你需要更高等级的类型来写下这个函数的类型签名。例如。在 Haskell 中,它可以写成 g :: (forall a . a -&gt; a) -&gt; (b, c) -&gt; (b, c)。不幸的是,它们使类型推断无法确定,并且可能无法很好地与 CLR 配合使用,因此 F# 编译器有理由不支持它们。
    • 你说'传递的函数能够处理这些类型'那么函数的参数应该是基类或对象或元组中两种类型都实现的某个接口
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-05-07
    • 2011-06-22
    • 2017-07-05
    相关资源
    最近更新 更多