【问题标题】:Why Scala Infer the Bottom Type when the type parameter is not specified?为什么 Scala 在未指定类型参数时推断底部类型?
【发布时间】:2021-03-06 00:55:25
【问题描述】:

我想知道是否有人可以解释下面这个特殊情况下的推理规则,最重要的是它是理性/暗示?

case class E[A, B](a: A) // class E
E(2) // E[Int,Nothing] = E(2)

请注意,我可以写 E[Int](2)。对我来说重要的是为什么第二个参数类型被推断为Nothing(即底部类型)而不是说Any?为什么会这样?原因是什么?

只是为了给出一些上下文,这与 Either 的定义以及它对 Left 和 Right 的工作方式有关。两者都是根据模式定义的

final case class X[+A, +B](value: A) extends Either[A, B]

你在哪里实例化它,比如Right[Int](2),推断的类型是Right[Nothing, Int],扩展名为Either[Nothing, Int]

编辑1

这里有一致性,但我仍然可以找出理性。以下是具有逆变参数的相同定义:

case class E[A, -B](a: A)// class E
E(2) // E[Int, Any] = E(2)

因此,当它是逆变的时,我们确实有相同的东西,这使得所有行为或推理规则一致。但是我不确定这样做的合理性....

为什么不使用相反的规则,即在 Co-Variant/Invariant 时推断 Any 而在 Contra-Variant 时推断 Nothing

编辑2

根据@slouc 的回答,这很有意义,我仍然理解编译器在做什么以及为什么要做它正在做的事情。下面的例子说明了我的困惑

val myleft = Left("Error") // Left[String,Nothing] = Left(Error)
myleft map { (e:Int) => e * 4} // Either[String,Int] = Left(Error)

  1. 首先,编译器将类型修复为“肯定有效”以重用@slouc 的结论(尽管在函数的上下文中更有意义)Left[String,Nothing]
  2. 接下来编译推断 myleft 的类型为 Either[String,Int]

给定的地图定义def map[B](f: A => B): Either[E, B],只有当myleft实际上是Left[String,Int]Either[String,Int]时才能提供(e:Int) => e * 4

也就是说,我的问题是,如果要稍后更改,将类型固定为Nothing 有什么意义。

确实以下不编译

val aleft: Left[String, Nothing] = Left[String, Int]("Error")

type mismatch;
found   : scala.util.Left[String,Int]
required: Left[String,Nothing]
val aleft: Left[String, Nothing] = Left[String, Int]("Error")

那么我为什么要推断一个类型,这通常会阻止我对该类型的变量做任何其他事情(但肯定会在推断方面起作用),最终改变那个类型,所以我可以对推断类型的变量做一些事情。

EDIT3

Edit2 有点误会,@slouc answer 和 cmets 中都说明了一切。

【问题讨论】:

  • 谢谢你,我看到它触及了重点,但不知怎的,我无法很好地连接它。
  • 这是我没有将问题标记为重复的方式,而是在此处发布为仅供参考。
  • 我真的不明白你的意思是你以后必须改变它,或者为什么你说它会阻止你对它做任何事情? - 我也不明白你认为什么会更好以及为什么。 - 最后,我不确定你是否意识到,因为 Scala 是一种急切的语言,我们必须推断每个值 的所有类型(这在 Scala 3 中会有所改变b> 具有多态函数),而在 Haskell 中,类型在使用时保持开放以进行推断,请注意,在 Scala 中不同的另一个原因是子类型 (AFAIK Haskell 没有)
  • 最后,你通常没有 Nothing 类型的值,而是用来简化多个值在正确类型中的组合。

标签: scala generics type-inference covariance


【解决方案1】:
  • 协方差:
    给定类型F[+A] 和关系A <: B,则以下成立:F[A] <: F[B]

  • 逆变:
    给定类型F[-A] 和关系A <: B,则以下成立:F[A] >: F[B]

如果编译器无法推断出确切的类型,它将在协变情况下解析可能的最低类型,在逆变情况下解析最高可能类型。

为什么?

当涉及到子类型的变化时,这是一个非常重要的规则。它可以显示在 Scala 的以下数据类型的示例中:

trait Function1[Input-, Output+]

一般来说,当一个类型放在函数/方法参数中时,就意味着它处于所谓的“逆变位置”。如果在函数/方法返回值中使用它,则它处于所谓的“协变位置”。如果两者都存在,则它是不变的。

现在,鉴于本文开头的规则,我们得出结论:

trait Food
trait Fruit extends Food
trait Apple extends Fruit

def foo(someFunction: Fruit => Fruit) = ???

我们可以提供

val f: Food => Apple = ???
foo(f)

函数fsomeFunction 的有效替代品,因为:

  • FoodFruit 的超类型(输入逆变)
  • AppleFruit 的子类型(输出的协方差)

我们可以这样用自然语言来解释:

"方法foo 需要一个可以接受Fruit 并产生一个 Fruit。这意味着 foo 将有一些 Fruit 并且需要一个 它可以提供给它的功能,并期待一些Fruit 回来。如果它得到一个 功能Food => Apple,一切都很好 - 它仍然可以喂它 Fruit(因为函数可以取任何食物),并且可以接收 Fruit(苹果是水果,所以尊重合同)。

回到你最初的困境,希望这能解释为什么在没有任何额外信息的情况下,编译器将求助于协变类型的最低可能类型和逆变类型的最高可能类型。如果我们想为foo 提供一个函数,我们知道肯定有一个函数:Any => Nothing

Variance in general.

Variance in Scala documentation.

Article about variance in Scala(完全披露:我写的)。

编辑:

我想我知道什么让你感到困惑。

当您实例化Left[String, Nothing] 时,您可以稍后使用函数Int => WhateverString => WhateverAny => Whatevermap。这正是因为前面解释的函数输入的逆变性。这就是您的 map 有效的原因。

"如果要更改类型,将类型固定为 Nothing 有什么意义 以后呢?”

我认为在逆变的情况下,将未知类型固定为Nothing 的编译器有点难以理解。当它在协方差的情况下将未知类型修复为Any 时,感觉更自然(可以是“任何东西”)。由于前面解释的协变和逆变的对偶性,相同的推理适用于逆变Nothing 和协变Any

【讨论】:

  • 一切都有意义。请注意,我了解协方差与逆变。虽然对结论 Any => Nothing 表示赞赏。虽然它有帮助,但它进一步推动了我最初的问题。我将添加一个 EDIT 2,解释原因?
  • 我在 EDIT2 中澄清了我的问题
  • 是的,我快到了,而且做得更好。首先,我在 Edit2 中的推论是错误的。映射时类型确实没有改变,要映射的函数的类型实际上是 Nothing => B,您可以将 Int => B 传递给它。无论如何,这是所有事情的结果。但是在到达地图之前,您还有其他要说的。如果我错了,请纠正我....
  • 通过制作未指定的协变类型参数 Nothing,您可以确保它在任何情况下的协变,即 val e: [String, Int] = Left(""") 有效。所以我不得不从另一个角度看待这个问题,而不是我在问题中的例子。同样,如果未指定的类型是逆变的,通过将其设为 Any,我们可以确保它在任何情况下的逆变性......您的示例无法编译:) 实际上是输入参数的用例。
  • 是的,完全正确。方法map 接受一个函数Nothing => B1,其中B1 可以是任何东西,您可以使用(e: Int) => e * 4 提供它。这是函数参数逆变的一个完美示例 - 您可以传递函数 Int => Int,原因与在我的示例中我们被允许传递 Food => AppleAny => Apple 的原因相同。什么没有编译?如果这就是您所指的,我现在将添加缺少的特征。
【解决方案2】:

这是引自 Scala 中编译时和运行时元编程的统一 尤金·布尔马科(Eugene Burmako)

https://infoscience.epfl.ch/record/226166(第 95-96 页)

在类型推断期间,类型检查器收集关于缺失的约束 来自类型参数边界的类型参数,来自术语类型 参数,甚至来自隐式搜索的结果(类型推断 与隐式搜索一起使用,因为 Scala 支持模拟 函数依赖)。可以将这些约束视为 未知类型参数表示为的不等式系统 类型变量和顺序是由子类型关系强加的。

收集约束后,类型检查器开始一步一步 过程中,在每一步中,都试图将某种变换应用于 不平等,创造了一个等价的,但据说更简单的系统 不平等。类型推断的目标是转换原始的 不等式对等式表示的唯一解 原系统。

大多数时候,类型推断成功。在那里面 在这种情况下,缺少的类型参数被推断为由表示的类型 解决办法。

但是,有时类型推断会失败。例如, 当类型参数T 是幻像时,即在术语参数中未使用 该方法中,它在不平等系统中的唯一入口将是 L <: T <: U,其中LU 分别是它的下限和上限。 如果L != U,这个不等式没有唯一解,并且 表示类型推断失败。

当类型推断失败时,即 当它无法采取任何更多的转换步骤时,它的 工作状态仍然包含一些不等式,类型检查器中断 僵局。它需要所有尚未推断的类型参数,即那些 其变量仍然由不等式表示,并且强制 最小化它们,即将它们等同于它们的下限。这产生 精确推断出某些类型参数的结果,以及一些 被看似任意的类型所取代。例如, 无约束类型参数被推断为Nothing,这是一个 Scala 初学者常见的困惑来源。

您可以了解有关 Scala 中类型推断的更多信息:

Hubert Plociniczak 解密本地类型推断 https://infoscience.epfl.ch/record/214757

Guillaume Martres Scala 3,类型推理和你! https://www.youtube.com/watch?v=lMvOykNQ4zs

Guillaume Martres Dotty 和类型:到目前为止的故事 https://www.youtube.com/watch?v=YIQjfCKDR5A

幻灯片http://guillaume.martres.me/talks/

Aleksander Boruch-Gruszecki Dotty 中的 GADT https://www.youtube.com/watch?v=VV9lPg3fNl8

【讨论】:

    猜你喜欢
    • 2014-02-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多