Swift performs implicit type conversions in a few situations for convenience. While this normally interacts fine with noncopyable types, ever since the introduction of noncopyable generics, there are a few places where it can be confusing:
protocol Fooable: ~Copyable {}
struct Foo: ~Copyable, Fooable {}
func test(_ v: borrowing Foo?) {}
func conversions() -> any Fooable & ~Copyable {
let foo = Foo()
test(foo) // implicit conversion `foo as Foo?`
return foo // implicit conversion `foo as any Fooable & ~Copyable`
}
But, these conversions are consuming operations. So the program above has an error:
example.swift:7:7: error: 'foo' consumed more than once
5 |
6 | func conversions() -> any Fooable & ~Copyable {
7 | let foo = Foo()
| `- error: 'foo' consumed more than once
8 | test(foo)
| `- note: consumed here
9 | return foo
| `- note: consumed again here
10 | }
I think most people would look at test
and see that it's taking a borrowed argument, and become confused at how it's being consumed.
But, it's the caller conversions
that is doing an implicit conversion of foo
in order to pass it as an Optional, and conversions are consuming.
I'd like to hear what people think about this. Here are a few ideas to make this situation less confusing:
- Emit a warning when doing an implicit conversion, that is silenced by writing
test(consume foo)
. For example:
example.swift:8:8: warning: implicit conversion to 'Foo?' is consuming
6 | func conversions() -> any Fooable & ~Copyable {
7 | let foo = Foo()
8 | test(foo)
| |- warning: implicit conversion to 'Foo?' is consuming
| `- note: add 'consume' to silence this warning
9 | return foo
10 | }
The explicit consume would only be required in situations where the conversion is happening on a variable.
It's easy to avoid warning in situations like return foo
or test(Foo())
, as they are clearly last-use or not converting a variable. But, identifying last-uses in general requires control-flow analysis to avoid superfluous warnings like this:
func returnNothing() {
let foo = Foo()
// ...
test(foo) // warning: implicit conversion to 'Foo?' is consuming
return
}
I'll note that there is already a precedent for warnings about ambiguous things / common mistakes that can be silenced.
- The second approach is to make the implicit conversions, explicit. So, similar to (1), but instead of warning to add a
consume
, we can make it an warning (or perhaps an error) that you need to write the cast explicitly, liketest(foo as Foo?)
,test(.some(foo))
.