对功能子类型感到困惑

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对功能子类型感到困惑相关的知识,希望对你有一定的参考价值。

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

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

bool<int<real

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

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

答案

这是函数子类型的规则:

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

Co-variant ==为结果参数的类型保留“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的子类型(它是)。查看两个函数的参数类型(并使用<符号表示“是”的子类型),那么如果f2 <f1,则是{x,y,z} <{x,y}?答案是肯定的。 {x,y,z}与{x,y}共同变体。即在定义结构{x,y,z}时,我们从{x,y}结构“继承”,但添加了第三个成员z。

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

然而,第三种思考方式是假设f2 <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});
另一答案

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


子类型规则是:

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

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

有了这个定义,为什么这样的规则呢?在Aaron Fi的回答中说得很清楚,我也找到了定义here(搜索“功能类型”标题):

另一种观点是允许一种类型S1 → S2的函数在一个上下文中使用是安全的,只要在这种情况下可以传递给函数的参数都不会让它感到惊讶,就可以使用另一种类型的T1 → T2T1 <: S1)并且它返回的结果都不会令环境感到惊讶(S2 <: T2)。

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

对于以下所有示例,请:

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

Example with contravariant argument types and covariant return types

让:

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

现在假设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记录字段并且没有问题。

结论:

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

Example with covariant argument types and covariant return types

让:

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

假设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中的任何记录中。 ⚡

Example with contravariant argument types and contravariant return types

让:

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

假设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字段的记录列表,我们将遇到错误。 ⚡

Example with covariant argument types and contravariant return

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

结论

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

另一答案

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

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

以上是关于对功能子类型感到困惑的主要内容,如果未能解决你的问题,请参考以下文章

对 iOS 上 SpriteKit 中的坐标、帧和子节点感到困惑?

对减少和箭头功能感到困惑[重复]

在 Joomla 替代布局和使用 K2 模板之间感到困惑

对 uicontrol 的 nextresponder 感到困惑

基于时间复杂度的这些片段真的很困惑

新手 scala 对身份类型签名感到困惑,啥都没有(底部)