Function call type checking for operations
dvdvgt opened this issue · comments
Description
When calling operations, the argument types are of course checked against the declared type. But which actually is the declared type if there are multiple definition of an operation given by the following?
interface Eff[A] {
def op(x: A): A
}
Consider this object:
def impl: Eff[Int] = new Eff[Int] {
def op(x: Int) = x / 1
}
When calling impl.op("hello")
, the function type for the symbol of op
is looked up. However, the function type is annotated as [A](x: A): A
once. Thus, when checking the function call, the call is only ever checked with respect to the original declared type and not all potentially existing types of concrete implementations.
One solution could be to annotate each concrete type during typechecking the implementation of Eff
, but since we only have exactly one symbol for all possible implementations of op
, updating the type of op
overwrites the previous annotation.
Note that this is not an issue exclusive to objects, but to also for handlers.
try { eff.op("hello") }
with Eff[Int] { def op(x) = resume(x / 1) }
Both of these examples compile and yield NaN
.
Idea
We need to have unique symbols for each operation implementation and then annotate the specific type for each implementation to this unique symbol.
Relevant
-
We need to bind this type to the operation here:Context.bind(op.symbol.asOperation, <>)
. This overwrites previous annotations, though. -
effekt/effekt/shared/src/main/scala/effekt/Typer.scala
Lines 176 to 177 in 5dffce2
-
effekt/effekt/shared/src/main/scala/effekt/Typer.scala
Lines 910 to 912 in 5dffce2
Fun, you can even chain it! :)
interface Eff[A] {
def op(x: A): A
}
def eff1: Eff[Int] = new Eff[Int] {
def op(x) = x + 42
}
def eff2: Eff[String] = new Eff[String] {
def op(x) = eff1.op(x) ++ "!!!"
// => Should be an error, `x : String`, but `eff1.op` expects its argument to be `Int`
}
record Person(name: String, hairColour: String)
def main() = {
val jolene = Person("Jolene", "red")
println(eff2.op(jolene))
// => Should be an error, `jolene: Person`, but `eff2.op` expects its argument to be `String`
// prints "[object Object]42!!!"
}
This also, of course, breaks (my) reasoning about effects. 😭
interface Exc { def raise(msg: String): Nothing }
interface Eff[A] {
def op(x: A): A / Exc
}
def eff: Eff[Int] = new Eff[Int] {
def op(x) =
try { do raise("ohno") }
with Exc { def raise(msg) = println("error: " ++ msg) match {} }
}
record Person(name: String, hairColour: String)
def main() = {
val jolene = Person("Jolene", "red")
println(eff.op(jolene))
}
produces a compiler error with:
[error] test.effekt:1:1: Main cannot have user defined effects, but includes effects: { Exc }
interface Exc { def raise(msg: String): Nothing }
^
but Exc
has been handled already in eff
(verified in Core
:) )
Tangentially related:
In the following example, if I right-click on eff.op
and click on [Go to definition]
,
the cursor jumps to the declaration site in the interface.
This brings up the question: what is the definition of eff.op
? (Here's the obvious analogy to object-oriented languages: where do we jump there?)
I'd expect it to be the "closest" definition in the scope graph (especially if we can only go to a "single" definition...)
interface Eff[A] {
def op(x: A): A // <- currently it jumps here
}
def eff: Eff[Int] = new Eff[Int] {
def op(x) = x / 1 // <- but this is the "closer" definition
}
def main() = {
println(eff.op("Hello")) // /|\
// ^^ |
// [Go to definition] ------/
}
Getting back to the original question, actually the type of op
for this concrete callsite should not be [A](x: A): A
but (x: Int): Int
, obviously. It becomes even more tricky if both the type and the operation have a type parameter.
interface Eff[A] {
def op[B](x: B): A
}
def eff: Eff[Int] = new Eff[Int] {
def op[C](x) = x / 1
}
then at the callsite eff.op("Hello")
the type of op
should be [B](x: B): Int
where B
will be inferred to be String
.
The concrete implemention, however, should not matter. Only the fact that it is Eff[Int]
.
I think this substitution should be performed somewhere around here:
effekt/effekt/shared/src/main/scala/effekt/Typer.scala
Lines 957 to 960 in 5dffce2
That is before checkCallTo
.
Similarly, here we would somehow need to substitute the type parameters with unification variables since the receiver is not yet known:
We should definitely revisit the design decision to add type parameters like A
in your example as parameter to the operation
This was a trick in the beginning to treat every function-like thing uniformly, but seems ad-hoc now.