implicit conversions for Java types
See original GitHub issueI have long opposed defining any implicit type conversions in the language, because of the ambiguities and confusing behavior that can result. For example, consider what happens if we define an implicit type conversion from Java List
to Ceylon MutableList
, and write:
javaObject.someList.size
The programmer might expect size
to refer to the attribute MutableList.size
of type Integer
, but in fact it actually refers to the method size()
of List
. Even worse ambiguities arise if implicit conversions are transitive.
As we’ve seen, however, confusion and inconvenience can also arise from not having any sort of implicit type conversions, for example, when Java’s String
type leaks into Ceylon code, or when strings aren’t assignable to Java parameters that accept CharSequence
.
Yesterday, I realized that there is an approach to implicit conversions that doesn’t lead to the ambiguities I’ve always been bothered by. Traditional implicit type conversions are only applied when the original type is not assignable to the declared type of something. They are thus, in some sense, “lazy”, and the original type always has the chance to “leak out”.
But for the specific case of inter-language interop, that’s actually not what we need. What if we defined some type conversions that were always applied, as soon as possible? That is, as soon as we have an expression of type java.lang.String
, the conversion to ceylon.language::String
is immediately applied, and no operations of java.lang.String
are ever visible. Then, essentially, no Ceylon code would ever be able to interact with the type java.lang.String
except to the extent that it can occur as a type argument in a generic type.
Likewise, no Ceylon code would ever interact directly with a java.util.function.Predicate<T>
, since any expression of that type would immediately be converted to Boolean(T)
.
It seems to me that this solution is perfect for protecting Ceylon code from Java types, including String
, primitive wrapper classes, @FunctionalInterface
s, Iterable
, List
, Set
, Map
, and perhaps even primitive arrays (which could be converted to Array
). I think and hope it would also work for the special transformation CharSequence <-> List<Character>
, which I admit is a slightly tricky case.
So the next problem that arises is how to pass a value to a Java parameter declared with one of these “hidden” types. Since we can no longer form an expression of type java.lang.Integer
, we can’t call any Java method that accepts a java.lang.Integer
.
Well, the solution is quite similar: the typechecker, when it comes to assign a type to parameter of one of the “hidden” types, actually applies the same conversions, and arrives at a type to which we can assign!
Obviously, this implies a mathematical property of the transformation, namely that it is a bijection. Fortunately, I think we can shoehorn our transformations into a bijective function. However, I still need to verify that.
For this to really work correctly and transparently, there’s one big caveat: the set of transformations and reverse transformations would need to be applied every time we assign a supertype like Object
or Number
to or from Java! Actually CharSequence
also counts as one of these “supertypes”, so it’s not that special. As is Iterable
I guess.
Anyway, I think this is a very strong way forward, and just requires thinking through the details. I believe it can be made to work.
WDYT?
Issue Analytics
- State:
- Created 7 years ago
- Reactions:1
- Comments:33 (33 by maintainers)
Top GitHub Comments
Now that 1.3.0 is out, we can resume discussing those solutions. In a conversation with @gavinking he mentioned that the biggest drawback to auto-coercion is that it requires IDE support, because quick-fixes have to learn that coercion exists. To side-step the compatibility issue he proposes that this mapping is only turned on by a compiler flag.
If we do flags for type mapping, I don’t think we can do this per-module, as that would introduce incompatible types in hierarchies in different modules that have a different type mapping policy. But globally this should work. Note that a type mapping change that is optional means either that we can’t change how types are compiled, just how they appear in the model (which means a lot of work in the backend to untangle that), or that those things are reflected in how they are compiled and we run into erasure issues of having to figure out the proper erasure of a type depending on the type mapping policy that was in use in super-types (again a lot of work in the backend). Breaking compat would make this much easier for the backend, and frankly may be the only sane solution if we go this way.
If we’re going to do flags, I can also propose that the auto-coercion solution is turned on by flags, and can even be per-module or per-file (since it’s entirely turned on/off by the typechecker).
We could even support both options and let users pick.