【发布时间】: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 上进行测试)。
【问题讨论】: