【问题标题】:Scala: are parameterized types just a syntax sugar (as Odersky suggested) or not?Scala:参数化类型是否只是语法糖(正如 Odersky 建议的那样)?
【发布时间】:2018-07-08 06:42:00
【问题描述】:

为了更深入地了解 Scala 类型系统,我发现了 Martin Odersky 的这个(旧)演示文稿:

https://www.youtube.com/watch?v=ecekSCX3B4Q&t=3747s

大约在这部电影的时间位置 [1:00:00] Martin 解释说,Scala 中的参数化类型实际上只是语法糖,您可以完全重写代码,用抽象类型替换它们。作为这种翻译的副作用,我们得到了对“类型差异”的严格解释。哇。听起来很不错,整个故事和我在代码中发现的一些问题非常对应,所以我立即开始尝试。但是,你猜怎么着——这种转换没有按预期工作。或者我做错了什么。

这是我用来隔离问题的一小段代码:

import java.net.URL

trait MessagingClient[BrokerLocation] {
  def connect(broker: BrokerLocation)
  def sendMessage(targetNodeAddress: Long, msg: Any)
}

class KafkaMessaging extends MessagingClient[URL] {
  override def connect(broker: URL): Unit = ???
  override def sendMessage(targetNodeAddress: Long, msg: Any): Unit = ???
}

class ClusterNode[BrokerLocation](messagingClient: MessagingClient[BrokerLocation]) {
  def startNode(brokerLocation: BrokerLocation): Unit = {
    messagingClient.connect(brokerLocation)
  }
}

object Test {
  def main(args: Array[String]): Unit = {
    val messagingClient = new KafkaMessaging
    val clusterNode = new ClusterNode[URL](messagingClient)
    val brokerLocation = new URL("http://1.2.3.4:666")
    clusterNode.startNode(brokerLocation)
  }
}

上面的代码编译没有问题。现在,这是我第一次尝试消除参数化类型:

import java.net.URL

trait MessagingClient {
  type BrokerLocation
  def connect(broker: BrokerLocation)
  def sendMessage(targetNodeAddress: Long, msg: Any)
}

class KafkaMessaging extends MessagingClient {
  override type BrokerLocation = URL
  override def connect(broker: URL): Unit = ???
  override def sendMessage(targetNodeAddress: Long, msg: Any): Unit = ???
}

class ClusterNode(val messagingClient: MessagingClient) {
  def startNode(brokerLocation: messagingClient.BrokerLocation): Unit = {
    messagingClient.connect(brokerLocation)
  }
}

object Test {
  def main(args: Array[String]): Unit = {
    val messagingClient = new KafkaMessaging
    val clusterNode = new ClusterNode(messagingClient)
    val brokerLocation = new URL("http://1.2.3.4:666")
    clusterNode.startNode(brokerLocation)
  }
}

但是,这种尝试不起作用。 Typechecker 似乎拥有批准输入所需的所有信息,但它仍然抱怨该行:

clusterNode.startNode(brokerLocation)

这次尝试失败了,我决定更严格地进行转换,即在之前参数化的每个类中引入抽象类型。令人惊讶的是,这个尝试也编译失败:

import java.net.URL

trait MessagingClient {
  type BrokerLocation
  def connect(broker: BrokerLocation)
  def sendMessage(targetNodeAddress: Long, msg: Any)
}

class KafkaMessaging extends MessagingClient {
  override type BrokerLocation = URL
  override def connect(broker: URL): Unit = ???
  override def sendMessage(targetNodeAddress: Long, msg: Any): Unit = ???
}

class ClusterNode(val messagingClient: MessagingClient) {
  type BrokerLocation = messagingClient.BrokerLocation
  def startNode(brokerLocation: BrokerLocation): Unit = {
    messagingClient.connect(brokerLocation)
  }
}

object Test {
  def main(args: Array[String]): Unit = {
    val messagingClient: MessagingClient = new KafkaMessaging
    val clusterNode = new ClusterNode(messagingClient) {type BrokerLocation = URL}
    val brokerLocation = new URL("http://1.2.3.4:666")
    clusterNode.startNode(brokerLocation)
  }
}

现在 - 错误来自哪里?我还试图找到参数化类型和抽象类型之间整体等价的“根”解释,但我在 Scala 语言规范中几乎找不到它。也许你们中的一些人已经设法调查了这个问题....

编辑 (作为安德烈长期调查后的后续行动......相当长的评论......不改变原来的问题,但对问题提出了一些额外的看法)

再次感谢 Andrey 对该主题的广泛研究。我还花了一些时间分析我所知道的并遵循您的提示。

首先是技术问题:我实际上将您的解决方案的最新版本('Cluster3')粘贴在一起,不幸的是它没有编译。花了一些时间来改进您的想法以解决问题。看:

abstract class MessagingClient {
  type BrokerLocation
  def connect(b: BrokerLocation): Unit
}

class KafkaMessaging extends MessagingClient {
  override type BrokerLocation = URL
  override def connect(broker: URL): Unit = ???
}

abstract class ClusterNode3 {
  val msgClient: MessagingClient
  type BrokerLocation = msgClient.BrokerLocation
  def connect(i: BrokerLocation): Unit =
    msgClient.connect(i)
}

object AndreySolution {
  def main(args: Array[String]): Unit = {
    val messagingClient = new KafkaMessaging

    //your original solution - unfortunately leads to compilation error in last line
    //val clusterNode = wrapMessagingClientIntoClusterNode3_param(messagingClient)

    //naive attempt to solve the problem by adding type annotation - does not help, really
    //val clusterNode: ClusterNode3 {type BlokerLocation = URL} = wrapMessagingClientIntoClusterNode3_param(messagingClient)

    //... but this actually works
    //val clusterNode: ClusterNode3 {val msgClient: KafkaMessaging} = new ClusterNode3 {val msgClient = messagingClient}

    //...and this also works - using the 'smarter' wrapping approach
    val clusterNode = wrapMessagingClient_byWojciech(messagingClient)

    val brokerLocation = new URL("http://1.2.3.4:666")
    clusterNode.connect(brokerLocation)
  }

  def wrapMessagingClientIntoClusterNode3_param[I](p: MessagingClient { type BrokerLocation = I} ): ClusterNode3 { type BrokerLocation = I } =
    new ClusterNode3 {
      val msgClient = p
    }

  def wrapMessagingClient_byWojciech[T <: MessagingClient](p: T): ClusterNode3 {val msgClient: T} = new ClusterNode3 {val msgClient = p}

}

现在 - 最后的想法。我在这里认识到两个独立(但相互关联)的问题:

  • 问题 1(= 我的原始问题)参数化类型可以理解为抽象类型的语法糖吗? - 仍然不确定,特别是因为在我们的解决方案代码中,我们显然再次使用了参数化类型,而且这种用法感觉至关重要(实际上很有趣)
  • 问题 2:在同一源代码中组合路径相关类型和抽象类型可能比预期的要复杂得多。在花了几个小时试图找到我的方式之后,我仍然觉得这是“在冰上行走”。到目前为止,当我将抽象类型与路径混合使用时,我还无法得出一个明确的方案,哪些编码模式是安全的,哪些不是。

请看一下这段代码(相当随机的例子,取自我漫长的马拉松实验):

object Fancy {
  val fooWithInt = new Foo {type A = Int; val numberA = 1; type B = String; val numberB = "42"}
  val boxWithFooWithInt = new Box {type C = Int; val secret1 = fooWithInt; val secret2 = "bingo"}
  val surprise: Int = boxWithFooWithInt.secret1.numberA
  val secret: String = boxWithFooWithInt.secret2
}

trait Foo {
  type A
  type B
  val numberA: A
  val numberB: B
}

trait Box {
  type C
  type D = secret1.B
  val secret1: Foo {type A = C}
  val secret2: D
}

从人类思维的角度来看,这段代码中的类型是正确的。现在尝试猜测 - Scala 编译器会高兴还是不高兴?

所以,事实证明 Intellij 尖叫类型错误,但 Scala 编译器确认这次一切正常。对我来说,在求解路径依赖类型理论中的类型方程组时,编译器的推理能达到多远仍然有点像彩票。

最有可能得到我所面临问题的明确答案的人应该调查当前 Scala 编译器实现的实际类型理论。而且我现在也非常好奇 Dotty 将如何处理我的示例(也许我可以抽出时间在 Dotty beta 上进行测试)。

【问题讨论】:

    标签: scala generics types


    【解决方案1】:

    注意像这样的声明

    class Foo[A](arg: Bar[A])
    

    保证Foo的第一个类型参数和Foo的第一个类型参数 Bar 是相同的。在您的原始代码中,这一行

    class ClusterNode[BrokerLocation](messagingClient: MessagingClient[BrokerLocation])
    

    确保ClusterNode 和注入的messagingClient 同意BrokerLocation 的同一类型,此外,BrokerLocation 的类型作为ClusterNode[BrokerLocation] 类型的一部分从外部可见。

    在您的第一次尝试中,ClusterNode 甚至没有 一个抽象类型成员,所以关于类型成员的信息 messingingClient 立即丢失。

    在你的第二次尝试中,你写的大致对应

    class ClusterNode(val messagingClient: MessagingClient[_ <: Any]) {
      // once the type parameter of `messagingClient` is forgotten,
      // use `BrokerLocation` typedef to publish the absent 
      // information about `messagingClient`s type parameter.
      type BrokerLocation = Any
    }
    

    在类型参数语言中。也就是说,两者之间的联系 messagingClient 的类型和ClusterNode 的类型再次丢失。

    现在考虑这段代码:

    import java.net.URL
    
    trait MessagingClient {
      type BrokerLocation
      def connect(broker: BrokerLocation)
      def sendMessage(targetNodeAddress: Long, msg: Any)
    }
    
    class KafkaMessaging extends MessagingClient {
      type BrokerLocation = URL
      override def connect(broker: URL): Unit = ???
      override def sendMessage(targetNodeAddress: Long, msg: Any): Unit = ???
    }
    
    abstract class ClusterNode { self =>
      type BrokerLocation // type is declared
    
      // (*) Coherence is enforced
      val messagingClient: MessagingClient { type BrokerLocation = self.BrokerLocation }
    
      def startNode(brokerLocation: BrokerLocation): Unit = {
        messagingClient.connect(brokerLocation)
      }
    }
    
    object Test {
      def main(args: Array[String]): Unit = {
        val msgCl = new KafkaMessaging
        // (**)
        val clusterNode = new ClusterNode {
          type BrokerLocation = URL
          val messagingClient = msgCl
        }
        val brokerLocation = new URL("http://1.2.3.4:666")
        clusterNode.startNode(brokerLocation)
      }
    }
    

    查看标有(*) 的行周围的代码。 在它上面的代码行中,我们声明ClusterNode 有一些 名为BrokerLocation 的抽象类型成员,在它下面的行中, 我们强制 messagingClient 是 MessagingClient 和 兼容的抽象成员类型。一旦你这样做了,摘要 type 成员不会丢失,(**) 之后的行中的代码 按预期编译,即使实例化更多 很麻烦。

    编辑:显然还不清楚为什么“第二次尝试” 在MessagingClient 中丢失有关BrokerLocation 的信息。

    整体情况是这样的:有BrokerLocation的某种类型, 并且这个类型应该在三个不同的地方保持一致:

    • 从ClusterNode 的外部看
    • 作为ClusterNode的类型成员
    • 作为包装MessagingClient 的类型成员

    ClusterNode 应该以某种方式确保BrokerLocation 在外部是已知的 (step1 outside-to-cn),所以我们可以从 main 传递一个 BrokerLocation 的实例。

    此外,应该以某种方式确定BrokerLocation 在ClusterNode 中与MessagingClient 中的相同(step2: cn-to-mc)。 在问题中,这条链条以两种不同的方式被打破:

    • step1 坏了,step2 坏了
    • 步骤 1 损坏,步骤 2 正常

    在帖子的第一部分,我只是简单地提出了一个变体,整个链条都可以工作,但我也替换了第二步:

    • 第 1 步正常,第 2 步再次正常(但实现方式不同)

    在下面的评论中,您认为有问题 在您的第二个提案中使用step2。不是这种情况。 组合“step1 ok, step2 broken”不是问题。 问题在于在构造函数中,因为它是在您的代码中编写的。 泄漏类型信息的是ClusterNode的构造函数, 从而破坏step1。

    我想保持 EDIT 可编译,所以让我们重复你第二次尝试的定义:

    abstract class MessagingClient {
      type BrokerLocation
      def connect(b: BrokerLocation): Unit
    }
    
    class ClusterNode(val mc: MessagingClient) {
      type BrokerLocation = mc.BrokerLocation
      def startNode(b: BrokerLocation) = mc.connect(b)
    }
    

    忘记 中声明的所有成员类型和方法 ClusterNode 的主体,看看构造函数 ClusterNode。 ClusterNode 构造函数的签名告诉你什么? 它告诉您它接受任何类型的MessagingClient,无论 BrokerLocation 的类型。里面什么都没有 ClusterNode 的声明阻止我们写这个:

    def wrapMessagingClientIntoClusterNode(p: MessagingClient)
    : ClusterNode = new ClusterNode(p)
    

    看看声明的第一行。 此方法签名中的任何内容都不会阻止您通过 所有类型的消息传递客户端,并且没有类型信息流 从第一行到第二行。

    编译器如何才能恢复任何有用的类型 将MessagingClient 中的BrokerLocation 传递给 ClusterNode?它无法恢复类型。它不会。 一旦调用构造函数,类型信息就会丢失。 ClusterNode 正文中没有多少类型声明和方法 可以恢复此信息。

    我们能否以某种方式将更多信息附加到ClusterNode,以便类型为 保存? 嗯,是的,但是我们必须附加以下相当繁琐的构造 从外面看:

    def wrapMessagingClientIntoClusterNode_param[I]
    (p: MessagingClient { type BrokerLocation = I } )
    : ClusterNode { type BrokerLocation = I } = 
      (new ClusterNode(p)).asInstanceOf[ClusterNode { type BrokerLocation = I }]
    

    注意这里不能省略asInstanceOf 部分,因为构造函数 本身泄露了必要的类型信息,就像第一个版本的 wrapMessagingClientIntoClusterNode 方法。

    现在将其与以下定义进行比较:

    abstract class ClusterNode2 { self =>
      type BrokerLocation
      val msgClient: MessagingClient { 
        type BrokerLocation = self.BrokerLocation 
      }
      def startNode(i: BrokerLocation): Unit = 
        msgClient.connect(i)
    }
    

    如果您尝试以与上述相同的方式编写方法wrapMessagingClientIntoClusterNode,会发生什么?

    def wrapMessagingClientIntoClusterNode2(p: MessagingClient)
    : ClusterNode2 = new ClusterNode2 {
      type BrokerLocation = p.BrokerLocation
      val msgClient = p
    }
    

    这或多或少和wrapMessagingClientIntoClusterNode方法一样没用,它也输了 类型信息,因为我们本质上是在要求一个存在类型MessagingClient[_] 在左手侧。但这一次,我们可以更轻松地纠正它:

    def wrapMessagingClientIntoClusterNode2_param[I]
    (p: MessagingClient {type BrokerLocation = I})
    : ClusterNode2 { type BrokerLocation = I } = 
      new ClusterNode2 {
        type BrokerLocation = I
        val msgClient = p
      }
    

    再次,我们引入一个类型参数I,它可以隧道类型 从参数部分到返回类型部分的信息。 它可以编译,但这次没有asInstanceOf,因为我们指定了 type BrokerLocation 成员优先,在构造函数忘记之前。

    现在,考虑第三个版本,它更接近您在 第二次尝试:

    abstract class ClusterNode3 { self =>
      val msgClient: MessagingClient
      type BrokerLocation = msgClient.BrokerLocation
      def connect(i: BrokerLocation): Unit =
        msgClient.connect(i)
    }
    

    你可以用它犯同样的愚蠢错误:

    def wrapMessagingClientIntoClusterNode3(p: MessagingClient)
    : ClusterNode3 = new ClusterNode3 {
      val msgClient = p
    }
    

    但是您也可以做正确的事情,并保留类型:

    def wrapMessagingClientIntoClusterNode3_param[I]
    (p: MessagingClient { type BrokerLocation = I} )
    : ClusterNode3 { type BrokerLocation = I } =
      new ClusterNode3 {
        val msgClient = p
      }
    

    这与您所做的几乎相同。只是没有构造函数。

    无论你采取ClusterNode2还是ClusterNode3都不是那么重要, 从ClusterNode3 切换到ClusterNode2 在 我回答的第一部分。消除“邪恶”类型擦除构造函数 其实更重要,因为它修复了step1。

    我会尝试总结一下:在您的第二次尝试中,编译器知道MessagingClient 和ClusterNode 中的BrokerLocation 类型成员是相同的,但它不知道类型是什么.

    我希望构造函数的作用现在清楚一点。

    【讨论】:

    • 非常感谢安德烈的回答。你肯定推动了我对 Scala 的理解。虽然这个故事对我来说仍然有点棘手。您设法指出我的错误,因为认为“type BrokerLocation = messingingClient.BrokerLocation”这一行将类型之间所需的方程式引入了类型检查器的状态。显然不是,但为什么呢?这是 Scala 编译器的弱点吗?一遍又一遍地看着这一点,我的感觉是,引入“连贯性”的两种方式——你的和我的——都应该奏效。
    • @WojciechZaborowski 实际上有两个相互作用的错误来源。你有(wrongA, okB)。我提出了(okA, anotherOkB)。您现在想知道为什么我将okB 替换为anotherOkB。这不是那么重要:他们都很好。相反,您应该关注为什么wrongA 是错误的,以及okA 如何纠正它。诚然,我可能造成了混乱,花了更多的精力来查明您的版本到底在哪里失败,而不是用一些完全不同的解决方案完全替换它。我更新了我的答案。抱歉,我没有精力让它更短:]
    • @WojciechZaborowski [续],但我真的试过了,我已经重写了 3 次编辑^^ 来自 EDIT 的代码 sn-ps 放在一起时是可编译的。
    猜你喜欢
    • 2019-05-07
    • 2016-01-26
    • 2013-01-19
    • 2016-07-25
    • 2014-08-10
    • 1970-01-01
    • 2011-03-09
    • 1970-01-01
    • 2022-11-29
    相关资源
    最近更新 更多