Automatic Mutable Pointer Conversion

Rather than see this pitch wither on the vine, I’d like to try to keep things moving. It took an unexpected twist when Chris intervened and raised the bar to “could a design be found where implicit conversions be expressed in Swift rather than a sequence ad-hoc changes to the compiler itself” all this without slowing type checking down. I have come to be strongly supportive of this goal.

To this end, I’ve produce a special “Friday the 13th” toolchain making a start on implementing this feature and been unable to record it slowing the compiler down benchmarking repeated builds of the fairly substantial GitHub - grpc/grpc-swift: The Swift language implementation of gRPC. project.

https://drive.google.com/file/d/1wUgTREO6LeMxdqXsoCeotTNF3O81Gnb-/view?usp=sharing

The toolchain is the “diff” of the original PR applied to tag swift-5.5-DEVELOPMENT-SNAPSHOT-2021-08-11-a which means it is most closely related to the Swift in recent Xcode betas.

Resistance to this feature seems to come from two quarters I’d characterise as “Should we” and “Could we”. Opinions vary widely about whether implicit conversions could be a good thing, the common criticism being that they affect the coders ability to anticipate what code is executed by a given expression but I would argue the ship has already sailed on this in Swift (I’m talking about you computed properties, not to mention willSet and didSet observing patterns or destructors which can fire at practically any time for that matter.) Like operator overloading introducing implicit conversions is a double edged sword and could certainly be open to abuse but it’s part of moving functionality out of the compiler developers hands into the Swift space. There are already implicit conversions and there are pitches for more about but they can/should be expressed in Swift.

The second objection “Could we” which I suspect colours the first objection in some people's eyes I hope to put to bed with the prototype toolchain. Although the toolchain takes a naive approach grafted onto the last phase of type checking this may in fact be a feature. The type checker and its internal constraint solver is a powerful abstraction at the heart of Swift but in this case it may be holding us back. Let’s face it, as a result Swift is a bit precious about strict typing when there are safe conversions it could be performing. This would make developers lives easier as in this pitch for example as it started out or recently for CGFloat/Double convertibility or even just up-typing smaller integer types to Int. if this feature can in fact be accommodated by modifying the constraint solver, all well and good but that is out of my competence.

And so to the “design” of the feature in the prototype. It is very simple. To create an implicit conversion you add an initialiser in an extension in the “to type” that takes the “from type” as an argument with a label implicit:. This design, using initialisers rather than conversion functions on the “from type” was a by-product of the implementation which re-uses the CGFloat<->Double code but it also seems to group conversions more intuitively. An example:

extension Int {
  init(implicit: Int16) {
    print("Int.init(implicit: Int16)")
    self.init(implicit)
  }
  init(implicit: Int32) {
    print("Int.init(implicit: Int32)")
    self.init(implicit)
  }
}
extension Int32 {
  init(implicit: Int8) {
    print("Int32.init(implicit: Int8)")
    self.init(implicit)
  }
  init(implicit: Int16) {
    print("Int32.init(implicit: Int16)")
    self.init(implicit)
  }
}

Note that the implementation does not chain conversions which constrains the search space. In this case Int8 is convertible to Int32 and Int32 is convertible to Int but Int8 is not convertible to Int as a result. As a result you can now make cyclic (i.e. non-DAG) conversions to make types interchangeable such as the new implementation of CFFloat<->Double:

extension Double {
  init(implicit: CGFloat) {
    print("Double.init(implicit: CGFloat)")
    self.init(implicit)
  }
}
extension CGFloat {
  init(implicit: Double) {
    print("CGFloat.init(implicit: Double)")
    self.init(implicit)
  }
}

A crinkle in the existing operation of the type checker is that to find a solution to coerce to use the new implicit constructor there needs to be an existing unlabelled conversion between the two types. If you want to make a completely new relationship between two types you need to provide such an initialiser. So, for example to make Doubles interchangeable with a Bool, you use the implicit label but in this case hidden:

extension Bool {
    init(_ implicit: Double) {
        self.init(implicit != 0.0)
    }
}

func smothingOperation(param: Double) {
    print("Smoothing \(param)")

}

let smoothingParam = 99.1
if smoothingParam {
    smothingOperation(param: smoothingParam)
}

This can all be bike-shedded and many will likely prefer a new @implicit annotation instead. I won’t pretend that crashing the toolchain isn’t possible as there is more work to do on handling generics but it serves its sole purpose: to prove that we should keep this conversation moving.

5 Likes