对功能子类型感到困惑

时间:2009-07-19 17:23:55

标签: types programming-languages type-theory

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

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

bool<int<real

为什么函数(real->bool)(int->bool的子类型?不应该是相反的吗?

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

4 个答案:

答案 0 :(得分:6)

这是函数子类型的规则:

参数类型必须是反变量,返回类型必须是共变体。

共变量==为结果参数的类型保留“A是B的子类型”层次结构。

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

所以,在你的例子中:

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

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

换句话说(我想到这个主题的简单英语方式):

反变量论点:“我的调用者可以传递更多而不是我要求,但这没关系,因为我只会使用我需要使用的东西。” 共同变量返回值:“我可以返回更多而不是调用者要求,但是没关系,他/她将只使用他们需要的东西,而忽略其余的”

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

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

所以在这里,我们断言f2是f1的子类型(它是)。查看两个函数的参数类型(并使用&lt;符号来表示“是”的子类型),那么如果f2&lt; f1是{x,y,z}&lt; {x,y}?答案是肯定的。 {x,y,z}与{x,y}共同变体。即在定义struct {x,y,z}时,我们从{x,y}结构“继承”,但添加了第三个成员z。

查看两个函数的返回类型,如果f2&lt; f1,然后是{x,y}&gt; {X,Y,Z}?答案是肯定的。 (见上述逻辑)。

考虑这个问题的第三种方法是假设f2&lt; f1,然后尝试各种铸造场景,看看是否一切正常。示例(伪代码):

   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});

答案 1 :(得分:5)

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

子类型规则是:

function subtyping rule

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

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

除了定义之外,为什么规则是这样的?在Aaron Fi的回答中说得很清楚,我还找到了here的定义(搜索标题&#34;功能类型&#34;):

  

另一种观点是允许一种类型的功能是安全的   S1 → S2用于需要其他类型T1 → T2的上下文中   只要在这个上下文中没有任何可以传递给函数的参数会让它感到惊讶(T1 <: S1)并且它返回的结果都不会令上下文感到惊讶(S2 <: T2)。

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

对于以下所有示例,请:

  1. S1 := {x, y}
  2. T1 := {x, y, z}
  3. T2 := {a}
  4. S2 := {a, b}
  5. 逆变量参数类型和协变返回类型的示例

    让:

    1. f1的类型为S1 → S2 ⟹ {x, y} → {a, b}
    2. f2的类型为T1 → T2 ⟹ {x, y, z} → {a}
    3. 现在假设type(f1) <: 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记录字段并没有问题。
      3. 结论:

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

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

        让:

        1. f1的类型为T1 → S2 ⟹ {x, y, z} → {a, b}
        2. f2的类型为S1 → T2 ⟹ {x, y} → {a}
        3. 假设type(f1) <: 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}
          3. 假设type(f1) <: 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} ]
            

            传入z时忽略f1记录字段没有问题,但是如果调用map的上下文需要b的记录列表字段,我们会遇到错误。 ⚡

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

            请看上面两个可能出错的地方的例子。

            结论

            这是一个非常冗长而冗长的答案,但我不得不记下这些东西,以弄清楚为什么其他参数和返回参数子类型无效。由于我有点写下来,我想为什么不在这里发布。

答案 2 :(得分:0)

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

以下代码将失败,因为您只能将字符串传递给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);

答案 3 :(得分:0)

我一直在为这个问题寻找自己的答案,因为我只是接受替换规则而没有找到直观。所以这是我的尝试:

按定义:如果f1: A1 => B1可以在需要f2: A2 => B2的地方使用,则函数f2f1超类型

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

enter image description here

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

  • 输入直径A2必须不小于A1
  • 输出直径B2必须小于{} 。

相同的推理适用于函数:为了使B1能够替换f2,然后:

  • f1的输入集A2需要涵盖f2的所有可能的输入,即f1 <:A1
  • A2的输出集B2必须符合f2的要求,即f1 <:B2

换句话说:如果B1 <:A1A2>:B1,然后B2>:f1: A1 => B1