Generalization of Implicit Conversions

Yes, I think you are right, this case might not be particularly interesting one for the language, although it's indeed implemented as an implicit conversion. T? -> T! is more interesting but it's covered by T! -> U!, so I'm just going to remove it from the list.

3 Likes

In an earlier thread @jrose mentioned the current compiler already supports implicit conversion like UnsafePointer<Int8> -> UnsafeRawPointer for function call arguments. I wonder if that should be in the above list also?

Sure, I have updated the bullet point that talks about pointers and added some more examples.

This is not universal, right? I think this only works when calling a closure with a tuple parameter. Are there other times this works?

If I remember correctly also in case matching and other patterns. It used to be any subtype relationship but @Slava_Pestov would know better.

This thread is about type conversions that succeed. Therefore I want to carefully bring this phenomenon to your attention and ask if it would be somehow related?!

While the conversions are not implicit, they still succeed.

1 Like

Thanks, @DevAndArtist! This discussion was intended to be more about merits and possible solutions for generalization of implicit conversion than just trying to meticulously enumerate all of them :slight_smile:

1 Like

I get that, but identifying current implicit conversions and comparing how they work (and how they differ) is an important part of conceptualizing a generalization that would later encompass them all, do you think?

I consider some of them like tuple-tuple to be a part of generic type conversion, that’s why I haven’t listed some conversions specifically, other examples include - deep equality, T -> Any or AnyObject, superclass conversion etc.The list in description only contains what compiler handles as implicit conversions, separate from normal conversion and subtyping.

What about #file, #fileID and #filePath? They can be used in placed where a String or StaticString is expected. Maybe that is just implemented as an overload and not as an implicit conversion because StaticString -> String or String -> StaticString does not work elsewhere. However, #file and friends are documented to return a String and not a StaticString: Expressions — The Swift Programming Language (Swift 5.5)

These are literals. More specifically, I believe #file and friends are a special kind of string literal (except for #line and #column which are special integer literals). They have a default type of String, but the inferred type can be anything that conforms to ExpressibleByStringLiteral.

12 Likes

To try to keep this thread moving I’d like to re-state my previous thoughts on generalized conversion, this time in the form of a proposal. Apologies to those who have seen these before but I’ve made minor changes to the design I discussed previously.

Generalization of Implicit Conversions

During the review process, add the following fields as needed:

Introduction

It's proposed that a mechanism be added by a relatively small change to the Swift compiler to express the implementations of conversions from source to destination types which can be automatically applied by the compiler as it type-checks an expression. These conversions should be expressed in the Swift language itself and have as near as possible to zero cost to users who do not make use of the feature.

Related Swift-evolution threads: Automatic Mutable Pointer Conversion Pitch: Implicit Pointer Conversion for C Interoperability Generalization of Implicit Conversions

Motivation

While Swift is capable of perhaps unprecedented expressiveness in its expressions it is very strictly typed and incapable of automatically making some of the smallest conversions between distinct types even when such a conversion would be lossless. This is a source of friction when interacting with data derived from C apis and a source of frustration to new users of Swift who are used to C's less rigid rules. One encounters this when trying to compare an Int32 with and Int for example or trying to pass an Int32 to a function that expects an Int. A means needs to be made available where the compiler can convert a less precise type to a more precise type with the user having to explicitly add a "cast" to match the types using an initialiser.

It is possible to think of many such implicit conversions who's presence Swift would benefit from but the tendency until now has been to express these inside the compiler itself disempowering the average Swift developer and progressively adding complexity to the type checking operation and time spent compiling.

Proposed solution

In it's simplest this conversion could be expressed in the following form as a new initializer (with the attribute @implicit) on the destination type:

extension ToType {
	@implict init(from: FromType) {
		self.init(from)
	}
}

Detailed design

A concrete example of this feature in operation would be adding the following extension to the standard library:

extension Int {
  @implict init(from: Int8) {
    self.init(from)
  }
  @implict init(from: Int16) {
    self.init(from)
  }
  @implict init(from: Int32) {
    self.init(from)
  }
}

In a prototype it has been found it is then possible to pass Int8 values to functions expecting an Int and make comparisons between Int8 and Int values with requiring a "cast" of the less precise type as, operators are implemented as functions in the Swift lanaguge. In using the prototype it was not possible to measure any slowdown of the compiler with these conversions in place with repeated compilations of a reasonably large open source project. This may be due to a feature of the specific implementation which will discussed in detail later.

Another example would be a new potential implementation of the bi-directional conversion between CGFloat and Double.

extension Double {
  @preferred_implict init(from: CGFloat) {
    self.init(from)
  }
}
extension CGFloat {
  @implict init(from: Double) {
    self.init(from)
  }
}

Such a bidirectional conversions can create ambiguities for the compiler however as were one to compare a CGFloat to a Double the compiler does not know whether to convert the CGFloat to a Double and compare Doubles or vici verca. In this very rare (perhaps unique) case a means has to be found to specify that one direction is the preferred conversion perhaps using yet another attribute @preferred_implict as shown in this example.

A final example could be a developer working extensively with C strings in Swift in a particular project might find it useful to add the following conversions:

extension String {
    @implict init(_ from: UnsafeMutablePointer<Int8>) {
        self.init(cString: from)
    }
    @implict init(_ from: UnsafePointer<Int8>) {
        self.init(cString: from)
    }
}

Note it is not possible to express the reverse conversion as an initializer cannot express adequately the lifetime of the resulting pointer. Also note that the initializer is unlabelled in this case as, to reuse the current implementation of the type checker an unlabelled initializer needs to be available. More on this later.

Whether developers can be trusted with this expressivity is an open question which a decision on which is at the core of whether this proposal should proceed at all. I would counter this is a debate which often comes up in in relation to whether the presence of operator overloading enhances a language. I personally am in favour of trusting the developer and empowering them over concerns about potential overuse. Swift does include operator overloading to implement it's operators and despite this I'm not aware of such a powerful feature being a frequent source of incomprehension when approaching another developer's code base due to over use. It is typically, simply not used other than to implement the Swift language itself by library developers as is the intended audience of the implicit conversion feature feature. This reservation might be assuaged by ensuring implicit conversions are properly scoped by applying existing access control mechanisms. One example of this might be that it could be applied is to require that @implicit public init()'s, i.e. those that library developer is exporting only be allowed on a type defined in that module to limit "leakage" of implicit conversions.

If the feature were to be made available, is it possible it could be used to re-implement some of the implicit conversions already built into the Swift compiler? The answer seems to be "yes and no". For example, it may be possible to express the promotion of a non-optional value to an optional in Swift but it would not however be possible to express the conversion of a named tuple to an unnamed tuple in a generalized manner. In my opinion it may not be useful to pursue this anyway as the C++ code is already there and such a refactoring many serve little purpose and may introduce subtle regressions. It is intended the feature should be primarily additive.

Implementation

It is worth reflecting for a moment on the specifics of the implementation of the prototype fleshed out in the PR as this will help refute the other common reservation about adding implicit conversions: that they will result in slower type checking performance. To discuss this, let's break type checking into it's two distinct phases. The first, most time consuming operation is funding solutions for a particular expression in terms of concrete functions to call implemented by specific types. In the Swift compiler, this is performed by a very abstract "constraint solver". After this, a solution may have been found but it may need to be "fixed" or "repaired" by "applying" one or other of the conversions built into the compiler.

For example, for an expression trying to call a function expecting an Int with an Int32 value it seems (I'm not the expert) the constraint solver identifies that an unlabelled initializer for Int taking an Int32 argument is available but doesn't view this solution as being "viable". So, it is already performing the work but giving up at that point and logging an error. The implementation of the prototype simply adds a "fix"/"repair" if an @implicit initialiser is available making the solution viable after the initializer has been "applied".

This led me to conclude that type checking is not being slowed down and indeed it was not possible to measure a slowdown with repeated builds of a fairly substantial open source project with and without the feature being available or in use. Keeping implicit conversions out of the constraint solver also prevents them from cascading leading to an exponential explosion of possibilities to check. It also makes possible bi-directional conversions. For more details on the prototype, consult the provisional PR mentioned above and the summary notes.

Source compatibility

This is an additive feature allowing source that would previously have failed to compile without an explicit conversion to compile in future versions of swift.

Effect on ABI stability

N/A as this is a source level feature and is additive.

Effect on API resilience

N/A as this is a source level feature and is additive.

Alternatives considered

Continuing to add add-hoc conversions to the C++ source of the compiler or not making more conversions available at all. An alternative form where conversions are expressed as an overloaded function on the FromType returning the ToType has been considered in the past and was in fact a feature of the early Swift betas but adding it as an extension to the ToType seems to group conversions better from the point of view of localising them and allows the implementation to re-use the type checker essentially as-is.

Acknowledgments

Swift

Edit: replaced @preferred @implicit with @preferred_implicit so as not to have an independent attribute @preferred.

Edit 2: Added idea of restricting public implicit conversions to be only possible if the module owns the type. This would preclude implementing the CGFloat <-> Double conversion using the new mechanism but we already have an implementation for that.

11 Likes

I like this proposal overall.

From a library designer's perspective, I would like to have the ability to declare an initializer as @explicit, so the user would be required to specify the type.

extension ToType {
	@explicit init(from: FromType) {
		self.init(from)
	}
}

let anInstance = FromType() as ToType
let anInstance: ToType = FromType() // error: Explicit conversion required

What benefit would @explicit give over the current conversion idiom of directly using an initializer?

let anInstance = ToType(FromType())
7 Likes

I'm quite cold to the idea of allowing a library to vend implicit conversions for types other than its own. But I'm not sure what to do about that.

I agree. I’m uneasy with the overall idea of unregulated implicit conversions, but even more so with implicit conversions not being controlled by the type's author. I suppose the latter concern can rather easily be solved by requiring that the @implicit inits be within in the type declaration (and not extensions thereof). There’s also precedent for this with the property-wrapper special inits init(wrappedValue:) and init(projectedValue:).

2 Likes

I'm sympathetic to these concerns though part of the fun is for for developers to be able to define implicit conversions on [standard] library types in their own codebase. As a compromise, perhaps a restriction could be introduced that an @implicit public init() (i.e. one that is exporting) can only be defined on a type in the same module.

This creates problems for representing the CGFloat <-> Double conversion as it is bi-directional and the types span two modules but it seems to be the exception rather than the rule and could be either special cased or we could use the existing hard coded implementation.

I think that implicit conversions should only be supported as part of subtype relationships, and it's the responsibility of the author of a type to define its supertypes. Given that, I think it makes sense to require the subtype definition to be at the type declaration, like we require for subclasses. Apart from CGFloat <-> Double (which I don't think should be generalised further), are there any use cases where we'd want bidirectional conversions?

Additionally, how does your proposed solution interact with as? casts? For example would you expect:

let x: Int32 = 0
let y = x as Any
let z = y as? Int

to work if there's an implicit conversion from Int32 to Int?

2 Likes

It's a implementation detail of the prototype toolchain I've been using to test these ideas out that prompting a conversion using an as does not work. Whether it should is an open question. Is it important to be able to use as when you can call the initialiser instead?

I'm specifically referring to dynamic casts with as?, rather than type coercion with as. For example, this should work with any implicit conversions:

func takesAnUnsafeRawPointer(_ x: UnsafeRawPointer) { _ = x }
let p = UnsafeMutableRawPointer(bitPattern: 0xFFFF)
takesAnUnsafeRawPointer(p)

but I would also argue that:

func someOtherFunction(_ x: Any) {
    if let x = x as? UnsafeRawPointer {
        takesAnUnsafeRawPointer(x)
    }
}

let p = UnsafeMutableRawPointer(bitPattern: 0xFFFF)
someOtherFunction(p)

should work; i.e. takesAnUnsafeRawPointer should be called.