Introduction
I recently learned of a feature/bug:
def foo(f: (x: Int) ?=> Int) = f(using 0)
foo(x.toString) // returns "0"
This seems like a neat feature, we don’t need to make these kinds of things anymore:
def ctx(using c: Context) = c
def foo(f: Context ?=> Int)
foo( ctx.iteration )
The issue is: The name of a function parameter is not supposed to matter !
val f: (x: Int) => x.type = (y: Int) => y
summon[((x: Int) ?=> x.type) =:= ((y: Int) ?=> y.type)] // works
Of course this divergence then creates issues:
type A = (x: Int) ?=> x.type
type B = (y: Int) ?=> y.type
def foo[T](y: T) = 0
summon[A =:= B] // works
// => A and B are equivalent, they can be swapped without effect
foo[A](x) // works
foo[B](x) // error: Not found: x
// They are not equivalent after all !
I think this perfectly illustrates what I will call the “name in type” problem
(see Using context function parameter by name (and not by type) - Question - Scala Users for more context)
The Problem
Sometimes, we name things in types because we to refer to them in the type itself, but we do not want this name to be relevant outside of it
This is the case with dependent types as highlighted above, and would be the case in qualified types (if they ever become part of the language)
Some other times, we name things in types because we want it to have some effect external to the type itself
In implicit functions, that name becomes available to refer to the given
In named tuples, the name becomes a constraint placed on types that should conform to it
These two different uses of names lead to surprising behavior like in the introduction, but not only:
val f: (a: (x: String, y: Int)) => ... =
(b: (x: String, y: Int)) => ...
// ^ can be different, but x and y cannot
And the answer for these inconsistencies is “That’s the order in which the features came out, if we could go back, we would do things differently”
The solution
We are at a very unique point in time:
- Named tuples are still experimental
- The “implicit functions put their parameters in scope” bug was just discovered (or at least discovered to be a bug ? It’s not documented anywhere)
- (qualified types are not part of the language)
This means we still have the opportunity to fix things !
Here are the 3 solutions that jump to me:
- All names in types are local: Drop named tuples and the bug
- Unlikely: named tuples have already been accepted for implementation
- All names in types are global:
(x: Int) => Int
and(y: Int) => Int
are no longer equivalent- Impossible: this would be break way too much existing code
- Differentiate local and global names through syntax, For example:
((x: Int) => A) =:= ((y: Int) => B)
but((x! Int) => A) =/= ((y! Int) => A)
- Named tuples are written
(x! Int, y! Int)
(x: Int) ?=> x.type
does not putx
in scope, but(x! Int) ?=> x.type
does- I used the exclamation mark as it is similar to, but more “striking” than, the colon, but I doubt it would work in practice
We should not underestimate this problem, as I believe it will keep creeping up more and more as we add more expressive types
Furthermore, we should find a solution right now, as there will probably not be another opportunity to fix this ! :insert Scala 4 joke here: