Trying to achieve deeper understanding of Scala type system, I found this (old) presentation of Martin Odersky:
https://www.youtube.com/watch?v=ecekSCX3B4Q&t=3747s
Roughly at time position [1:00:00] of this movie Martin explains that parameterized types in Scala are actually only a syntax sugar and you can completely rewrite your code replacing them with abstract types. As a side effect of this translation, we are getting a strict interpretation of "type variance". Wow. It sounded very nice and the whole story quite corresponded to some issues I found in my code, so I immediately started experimenting. But, guess what - this conversion does not work as expected. Or I am doing something wrong.
This is a very small piece of code I was using to isolate the problem:
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)
}
}
The code above compiles without issues. Now, this was my first attempt to eliminate parameterized types:
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)
}
}
This attempt does not work, however. Typechecker seems to have all needed information to approve the typing but nevertheless it complains about the line:
clusterNode.startNode(brokerLocation)
Failed with this attempt, I decided to be even more rigorous in doing the conversion, i.e. to introduce abstract type in every class that was previously parameterized. Surprisingly, this attempt also fails to compile:
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)
}
}
Now - where is the mistake coming from ? I also was trying to find the "root" explanation of the whole equivalence between parameterized types and abstract types but I could hardly find it in Scala language specification. Maybe some of you already managed to investigate this problem ....
EDIT (Added as a follow-up after Andrey long investigation ... rather long comment ... not changing the original question but throwing some extra light onto the problem)
Thanks again Andrey for your extensive research on the subject. I also spent some time analyzing what I know and following your hints.
Technical issues first: I actually pasted together the latest version of your solution ('Cluster3') and unfortunately it is NOT compiling. Took some time to improve your idea a little to fix the problem. Look:
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}
}
Now - final thoughts. I recognize here two separate (but connected) issues:
- Issue 1 (= my original question) Can parameterized types be understood as a syntax sugar for abstract types ? - Still not sure about it, especially because in our solution code we apparently fall into using parameterized types again and this usage feels crucial (so funny, actually)
- Issue 2: Combining path dependent types and abstract types in the same source code can be more tricky than one could expect. After spending several hours trying to find my way with this, I still feel like this is "walking on ice". So far I could not come to a clear recipe which patterns of coding are safe and which are not when I mix abstract types with paths.
Please have a look at this piece of code (pretty random example taken from my long marathon of experiments):
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
}
From the human thinking point of view types in this code are correct. Now try to guess - will Scala compiler be happy or not ?
So, it turns out that Intellij screams that types are wrong, but Scala compiler confirms that everything is fine this time. To me is it still kinda lottery how far the reasoning of the compiler can reach while solving the set of type equations in path dependent types theory.
Most likely to get the definitive answer to problems I am facing one should investigate the actual type theory that current Scala compiler implements. And I am now also super-curious how Dotty will handle my examples (maybe I can find time to test it on Dotty beta).