【问题标题】:Confused about function subtyping对函数子类型感到困惑
【发布时间】:2009-07-19 17:23:54
【问题描述】:

我正在学习编程语言课程,而“函数何时是另一个函数的子类型”的答案对我来说非常违反直觉。

澄清一下:假设我们有以下类型关系:

bool<int<real

为什么函数(real-&gt;bool)(int-&gt;bool 的子类型)?不应该反过来吗?

我希望子类型函数的标准是:f1 是 f2 的子类型,如果 f2 可以接受 f1 可以接受的任何参数,并且 f1 只返回 f2 返回的值。显然 f1 可以采用某些值,但 f2 不能。

【问题讨论】:

    标签: types programming-languages type-theory


    【解决方案1】:

    这是函数子类型化的规则:

    参数类型必须是逆变的,返回类型必须是协变的。

    Co-variant == 为结果参数的类型保留“A 是 B 的子类型”层次结构。

    Contra-variant == 反转(“反对”)arguments 参数的类型层次结构。

    所以,在你的例子中:

    f1:  int  -> bool
    f2:  bool -> bool
    

    我们可以有把握地得出结论,f2 是 f1 的一个子类型。为什么?因为 (1) 只查看两个函数的参数类型,我们看到“bool 是 int 的子类型”的类型层次结构实际上是协变的。它保留了 int 和 bool 之间的类型层次结构。 (2) 仅查看两个函数的结果类型,我们发现支持逆变。

    换一种说法(我对这个主题的简单英语方式):

    逆变参数:“我的调用者可以传入 更多 比我需要的参数,但这没关系,因为我只会使用我需要使用的东西。” 协变返回值:“我可以返回更多而不是调用者需要的,但这没关系,他/她只会使用他们需要的东西,而忽略其余部分”

    让我们看另一个例子,使用所有都是整数的结构:

    f1:  {x,y,z} -> {x,y}
    f2:  {x,y}   -> {x,y,z}
    

    所以在这里,我们再次断言 f2 是 f1 的子类型(它是)。查看两个函数的参数类型(并使用

    查看两个函数的返回类型,如果 f2 {x,y,z}?答案是肯定的。 (见上述逻辑)。

    第三种考虑这个问题的方法是假设 f2

       F1 = f1;
       F2 = f2;
       {a,b}   = F1({1,2,3});  // call F1 with a {x,y,z} struct of {1,2,3};  This works.
       {a,b,c} = F2({1,2});    // call F2 with a {x,y} struct of {1,2}.  This also works.
    
       // Now take F2, but treat it like an F1.  (Which we should be able to do, 
       // right?  Because F2 is a subtype of F1).  Now pass it in the argument type 
       // F1 expects.  Does our assignment still work?  It does.
       {a,b} = ((F1) F2)({1,2,3});
    

    【讨论】:

    • 抱歉,我的解释比我希望的要长。
    • 我还是有点困惑。如果 {x,y,z} 是 {x,y} 的子类型,为什么 bool 是 int 的子类型?在第一种情况下 {x,y,z} 大于 {x,y} 但在另一个示例中 bool 小于 int。
    • 因为您可以将 int 转换为 bool。 (无论如何,在大多数编程语言中。例如,将零视为假,将所有其他整数值视为真)。
    • 在我看来,这个问题的答案与那里的许多信息相冲突。子类型意味着逆变方法参数和协变返回类型,不是吗?换句话说,General->Specific 函数将是 Specific->General 函数的子类型(假设 Specific 是 General 的子类型)。
    • 这个答案不正确。这句话是倒退的:“参数类型必须是协变的,返回类型必须是逆变的。” @dbmiikus 的答案是正确的,应该是公认的答案。
    【解决方案2】:

    这是另一个答案,因为虽然我理解函数子类型规则的意义,但我想弄清楚为什么任何其他参数/​​结果子类型组合都没有。


    子类型规则是:

    意味着如果满足顶部子类型条件,则底部成立。

    在函数类型定义中,函数参数是逆变的,因为我们颠倒了T1S1 之间的子类型关系。函数结果是协变的,因为它们保留了T2S2 之间的子类型关系。

    既然定义不存在,为什么会有这样的规则? Aaron Fi 的回答中说得很清楚,我还找到了定义here(搜索标题“Function Types”):

    另一种观点是允许一种类型的函数是安全的 S1 → S2 用于需要另一种类型 T1 → T2 的上下文中 只要在此上下文中可能传递给函数的任何参数都不会让它感到惊讶(T1 &lt;: S1),并且它返回的任何结果都不会让上下文感到惊讶(S2 &lt;: T2)。

    再一次,这对我来说是有道理的,但我想看看为什么没有其他的打字规则组合是有意义的。为此,我查看了一个简单的高阶函数和一些示例记录类型。

    对于以下所有示例,让:

    1. S1 := {x, y}
    2. T1 := {x, y, z}
    3. T2 := {a}
    4. S2 := {a, b}

    具有逆变参数类型和协变返回类型的示例

    让:

    1. f1 有类型 S1 → S2 ⟹ {x, y} → {a, b}
    2. f2 有类型 T1 → T2 ⟹ {x, y, z} → {a}

    现在假设type(f1) &lt;: type(f2)。我们从上面的规则中知道这一点,但让我们假装我们不知道,看看为什么它是有意义的。

    我们运行map( f2 : {x, y, z} → {a}, L : [ {x, y, z} ] ) : [ {a} ]

    如果我们将f2 替换为f1,我们会得到:

    map( f1 : {x, y} → {a, b}, L : [ {x, y, z} ] ) : [ {a, b} ]
    

    这很好,因为:

    1. 无论f1 函数对其参数做什么,它都可以忽略额外的z 记录字段并且没有问题。
    2. 无论运行map 的上下文如何处理结果,它都可以忽略 额外的b 记录字段并没有问题。

    结论:

    {x, y} → {a, b} ⟹ {x, y, z} → {a} ✔
    

    协变参数类型和协变返回类型的示例

    让:

    1. f1 有类型 T1 → S2 ⟹ {x, y, z} → {a, b}
    2. f2 有类型 S1 → T2 ⟹ {x, y} → {a}

    假设type(f1) &lt;: type(f2)

    我们运行map( f2 : {x, y} → {a}, L : [ {x, y} ] ) : [ {a} ]

    如果我们将f2 替换为f1,我们会得到:

    map( f1 : {x, y, z} → {a, b}, L : [ {x, y} ] ) : [ {a, b} ]
    

    我们可能会在这里遇到问题,因为f1 期望并且可能对z 记录字段进行操作,而这样的字段在列表L 的任何记录中都不存在。 ⚡

    逆变参数类型和逆变返回类型的示例

    让:

    1. f1 有类型 S1 → T2 ⟹ {x, y} → {a}
    2. f2 有类型 T1 → S2 ⟹ {x, y, z} → {a, b}

    假设type(f1) &lt;: type(f2)

    我们运行map( f2 : {x, y, z} → {a, b}, L : [ {x, y, z} ] ) : [ {a, b} ]

    如果我们将f2 替换为f1,我们会得到:

    map( f1 : {x, y} → {a}, L : [ {x, y, z} ] ) : [ {a} ]
    

    当传递给f1 时,我们可以忽略z 记录字段,但是如果调用map 的上下文需要带有b 字段的记录列表,我们将遇到错误。 ⚡

    协变参数类型和逆变返回的示例

    查看上面的示例,了解可能出错的两个地方。

    结论

    这是一个非常冗长而冗长的答案,但我不得不记下这些内容以弄清楚为什么其他参数和返回参数子类型无效。既然我已经把它写下来了,我想为什么不把它贴在这里。

    【讨论】:

    • 谢谢,我对接受的答案感到困惑,因为他似乎使用了正确的逻辑但相反的术语。
    【解决方案3】:

    问题已得到解答,但我想在这里举一个简单的例子(关于参数类型,这是不直观的)。

    下面的代码会失败,因为你只能将字符串传递给myFuncB, 我们正在传递数字和布尔值。

    typedef FuncTypeA = Object Function(Object obj); // (Object) => Object
    typedef FuncTypeB = String Function(String name); // (String) => String
    
    void process(FuncTypeA myFunc) {
       myFunc("Bob").toString(); // Ok.
       myFunc(123).toString(); // Fail.
       myFunc(true).toString(); // Fail.
    }
    
    FuncTypeB myFuncB = (String name) => name.toUpperCase();
    
    process(myFuncB);
    

    但是,下面的代码会起作用,因为现在您可以将任何类型的对象传递给myFuncB, 我们只传递字符串。

    typedef FuncTypeA = Object Function(String name); // (String) => Object
    typedef FuncTypeB = String Function(Object obj); // (Object) => String
    
    void process(FuncTypeA myFuncA) {
       myFunc("Bob").toString(); // Ok.
       myFunc("Alice").toString(); // Ok.
    }
    
    FuncTypeB myFuncB = (Object obj) => obj.toString();
    
    process(myFuncB);
    

    【讨论】:

      【解决方案4】:

      我一直在努力寻找自己对同一问题的答案,因为我发现它直观只是接受替换规则。所以这是我的尝试:

      根据定义:如果f2 可用于需要f1 的地方,则函数f1: A1 =&gt; B1f2: A2 =&gt; B2超类型

      现在看下图,想象水从上到下通过漏斗f1f2

      我们意识到,如果我们想用另一个漏斗f2 替换漏斗f1,那么:

      • 输入直径A2 必须不小于A1
      • 输出直径B2 需要不大于B1

      同样的道理也适用于函数:为了让f2 能够替换f1,那么:

      • f2 的输入集A2 需要覆盖f1 的所有可能输入,即A1 <:>A2, 和
      • f2 的输出集 B2 需要适合 f1 的要求,即 B2 <:>B1

      换一种说法:如果A1 <:>A2 and B1 >: B2 then f1: A1 =&gt; B1 >: f2: A2 =&gt; B2

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-12-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-03-22
        • 1970-01-01
        相关资源
        最近更新 更多