As somebody who implemented SE-0307 I'd like to share a couple of insights about implicit conversions when it comes to a type-checker performance.
All of the following are empirically proven to lead to a bad performance:
- Allowing (pure) bi-directional conversions. In case of Double/CGFloat it led to ambiguities even in relatively simple expressions.
- Allowing multiple conversions in the same position e.g. converting from Double to CGFloat and back to Double.
- Allowing conversions in argument positions, especially for homogeneous operators (which allow of a sudden became heterogeneous) because it leads to unexpected results. Double/CGFloat managed to (somewhat) work around that by preferring Double over CGFloat.
I'm not going to re-iterate what @hborla and @codafi already said (and with which I agree), just want to add that while working on SE-0307 I did consider possible ways to implement that particular conversion in a more general way that could be extended to widening of integers (this is also discussed in pitch for Double/CGFloat conversion as possible future direction) but the problem is that even widening has big performance implications although it's scoped down to just a handful of types.
I think if we were to consider a general feature here we'd have to put so many restrictions on its syntactic use that it couldn't be called "general" in a normal sense and wouldn't be worth the complexity.
Now let's consider one possible solution as implemented in the associated PR. The idea is to add a non-failable initializer for each possible conversion type could have e.g.
extension UnsafeRawPointer {
init(implicit: UnsafeMutableRawPointer) { ... }
}
Could be used to implement uni-directional conversion from UnsafeMutableRawPointer
to UnsafeRawPointer
. Without bike-shedding to much on the syntax let's consider what the semantics might be:
- Any struct, class, enum (or nominal for that matter) should be convertible to other type if it has an initializer in form of
init(implicit: T)
where T
is the "from" conversion type.
Which means that every Conversion
(of any type e.g. argument, operator argument, regular) or Subtype
relationship in the type system now has to check whether right-hand side has an initializer that accepts left-hand type as an argument. This new rule could be expressed in a following set of constraints:
Starting from:
$From <convertible to> $To
Simplifies into:
$To[.member == init(implicit:)] == $Init
($From) -> $To <applicable function> $Init
Note that after lookup $Init
could be bound to a number of choices with different types of arguments (important - including failable initializers) if $To
is convertible to N different types.
After $Init
is determined it's possible to simplify ($From) -> $To <applicable function> $Init
into:
$From <argument conversion> $Init.$Arg0
$To <function result> $Init.$Result
Further simplification results in yet another [.member == init(implicit:)] ...
check for $Init.$Arg0
and $From
types if $To
has multiple overloads of init(implicit:)
.
It could be reasonable to disable followup checks in "implicit conversion" context but that would restrict the generality of the feature.
This new rule impacts all expressions in the language non-trivially but having to perform at least one additional member lookup per any kind of conversion, even if a type doesn't support implicit conversions. It could be argued that we could use a declaration attribute to mark some of the types as @implicitlyConvertible
to avoid having to pay the extra cost in the type-checker, but if we did that for some commonly used types e.g. to support widening conversions it would still impact a non-trivial amount of code and retroactively adding such an annotation might not be possible.
My suggestion would be to consider merit of an uni-directional conversion from UnsafeMutableRawPointer
to UnsafeRawPointer
just like we did Double/CGFloat and discuss general implicit conversions separately based on the knowledge gained from it and previous conversions.