What are the possible circumstances where having to use explicit initializer call would be preferred over the implicit one?
As a general rule, Swift does not have opt-in features (the big exception being library resilience, and transitional opt-ins such as exclusivity or language version modes). To do otherwise is fairly dangerous path, because it creates language variants, where one codebase can look radically different from another, permanently. You may even need both variants to exist in different packages in the same program.
Why would it be preferred to be an opt-in feature if there are no clear reasons to opt-out of it?
I sympathize, but the fact is the existence of CGFloat, and what it is and why it is and why it is messing up your simple arithmetic expressions for no apparent good reason, is just as strange as the compiler work we're suggesting putting in to hide it. Hence the need for several throat-clearing paragraphs at the start of this pitch. So I suspect it's a wash.
Fully on board with that.
My purpose is not to nitpick the implementation at all, but to point out that after reading this text and the implementation, they both point towards the laying of groundwork for a general-purpose mechanism for implicit value conversion; my point is only that such a sweeping vision doesn't seem necessary or justifiable on the basis of the (very legitimate) specific use case here.
If that's not the intention, then the clarification is appreciated. (I'll reach out out-of-band about the implementation aspects that give that impression.)
That's is not my intention, I'm concerned only with Double vs. CGFloat situation. Alternatives Considered section aimed (and apparently failed) to reinforce the position of a special-case for Double/CGFloat typealias by arguing that even widening conversions wouldn't cut it, and that's the only implicit conversions which might even be worth considering making a general purpose feature of the language as an opposite to just making it work for Double/CGFloat types.
I think that improving the usability of APIs that use CGFloat would be a welcome change for many developers. (More specifically, I think this would get a round of enthusiastic applause and shouts of glee if it were announced in a Whatâs New in Swift talk at a live WWDC).
I think that a number of points discussed in the thread should be added to the proposal:
- Explicitly call out that
CGFloatis a type that is defined by Apple frameworks, which makes this change specific to Apple platforms. (@scanon) - Reference that similar conversions, such as
CFType<->NSType, already exist in the compiler to improve interoperability ergonomics. (@hborla) - Emphasize that this is a targeted conversion to address a particular issue and is not intended to be the first step in a larger movement towards implicit conversions.
I believe doing so will make the proposal stronger by communicating that this is a very targeted change using a technique the compiler already uses in other cases, and is not some exotic new approach.
32-bit Behavior
Since watchOS still uses 32-bit floating point for CGFloat, I think it is important to add a section detailing the behavior on a 32-bit platform and a justification for the proposed behavior.
On 64-bit Apple platforms, moving between CGFloat and Double is essentially transparent with no loss of precision. The 64-bit case also seems very much in line with other affordances Swift has added to make it more ergonomic to use Apple frameworks.
But on watchOS, if a developer is working with a Double and passes it into an API that takes a CGFloat there will be an implicit narrowing conversion and a loss of precision.
Being able to write code where you can implicitly lose precision seems very un-Swift-like. The proposed behavior on 32-bit seems like quite an exception to typical Swift principles around numeric conversions and so I think it warrants being addressed in the proposal.
For instance, in âalternatives consideredâ, was it considered to have a warning about the conversion on 32-bit? Doing so would ensure that it compiles without building a 32-bit version to check, but still could alert the developer if they happen to be using the code on the 32-bit platform.
Or, alternately, it may be justified to implicitly lose precision in this particular case.
The Core Graphics APIs are designed to work well with single-precision float values on watchOS. Does a narrowing conversion from Double ever change the value enough to have a practical, noticeable, effect on what Core Graphics renders?
It also may be that, since Double is the preferred floating point type in Swift, that most watchOS developers working in Swift are already using Double and converting to 32-bit CGFloat values. Is that the case?
I donât have definitive answers to either of those, but the answers could help justify an implicit narrowing conversion in this particular case.
The trouble is, such a warning would be present on every conversion. What is the user to do at this point to suppress the error â put all the explicit conversions back? That would pretty much defeat the purpose of this improvement.
Perhaps the assumption here is that warnings only appear in places where "32-bit conversions" are happening. But that is determined by the choice of target, not by the code itself. So should these warnings only appear when building for that target? This is essentially a soft version of the rejected option 3 from the opening motivation.
Yes, I brought this up in the context of 'alternatives considered' because I think it would be good to include in the proposal that this was considered with reasons why it was rejected, as your post details nicely.
I think in a proposal review, some folks will have that question, so it would be good for the proposal to have it already asked and answered.
The trouble is, such a warning would be present on every conversion. What is the user to do at this point to suppress the error â put all the explicit conversions back? That would pretty much defeat the purpose of this improvement.
Perhaps the assumption here is that warnings only appear in places where "32-bit conversions" are happening. But that is determined by the choice of target, not by the code itself. So should these warnings only appear when building for that target? This is essentially a soft version of the rejected option 3 from the opening motivation.
If we accept that the same conveniences must be available when compiling for 32-bit and for 64-bit targets, then we must end up accepting that users on 32-bit platforms will lose warnings about implicit loss of precision. One directly implies the other.
The only way I can see to accommodate all of the disparate demands is with the addition of a new type. This is not such a burden for the end user because the ergonomic improvement provided by this proposal is enjoyed by newly (re-)written code.
Suppose, then, we choose not endow CGFloat with implicit narrowing interchangeability. Instead, we create a new type, CGDouble, which gets this magical interchangeability with Double (always lossless) and CGFloat (with potential loss of precision on 32-bit platforms). Users on both 32-bit and 64-bit platforms get an ergonomics win when they use CGDouble but are forewarned that the type supports implicit narrowing on 32-bit platforms, and users on both 32-bit and 64-bit platforms can continue to get warnings about potential loss of precision if they forget to write an explicit conversion from Double to CGFloat.
scanon:
That is ... a lot of calls. Itâs not just CG, itâs lots and lots of
other things throughout the SDK, plus itâs not just one new
overload, because there are operations with multiple
arguments (this could be âsolvedâ via a protocol, but that
has its own complications),
I guess I was a bit too imprecise about "having Apple solve this problem". I should have said "it's an Apple-specific problem and should ideally be fixed at that level. There is no reason why the open source Swift compiler team could not create that code. It's a bunch of additional methods, there is no need for changes to the compiler to solve that problem.
As to how much effort it is to write this code: There are tools like Sourcery that could be used to automatically create all of these methods (or maybe all but a few edge-cases).
plus return-type overloading is generally not ideal. None of
that is insurmountable, but itâs a lot of churn
Return type overloading is an issue that might crop up. I have to admit so far it worked for me, but then I might be unconsciously avoiding it more often, having done my share of Objective-C where that doesn't exist (and C++ where it kinda doesn't either).
That said, Swift can mark methods as unavailable. So one solution could be to mark all CGFloat calls with the moral equivalent of NS_SWIFT_REFINED based on a compiler switch, and only then pull in the compatibility lib that exposes them all as doubles. Then also typealias GFloat to double.
It just feels weird to add such a far-reaching change to the Swift compiler when the library could be fixed with existing facilities instead.
(And since that came up in the meantime: I'm not suggesting this be optionally on, but rather just that there exist a switch so people encountering issues with it in their code can turn it off)
I think weâre ignoring the elephant in the room: the unexamined question whether the following things are simultaneously possible:
-
A body of code that doesnât have to consider representational issues because it knows that
DoubleandCGFloatare effectively interchangeable. -
A body of code that functions correctly in terms of representation (precision) because it handles architectures where
CGFloatisFloatas well as architectures whereCGFloatisDouble.
They're not simultaneously possible. Case 2 requires explicit conversions, otherwise its behavior is unpredictable in subtle ways. Case 1 isnât worth having unless the conversions are implicit. But ⊠you canât do both of these at the same time.
Incidentally, the current conversion between CFType and NSType was mentioned up-thread. I think a better model is the current conversion between NSUInteger and Int. Swift imports Obj-C unsigned integer as a signed integer, damn the consequences! This wouldnât really work for CGFloat though, because loss of precision happens to be a nastier problem than the loss of half of NSUIntegerâs value range, but I'm using this idea as my starting point for the following.
However, I do agree with @xedin, @xwu and others that magically âfixingâ this in the Obj-C importer is an attractive approach.
My suggestion is that we go a bit deeper into magical territory:
-
Force the compiler to treat
CGFloatas an uninstantiable type in Swift. That means no explicit invocations of aCGFloatinitializer, and no properties/variables declared in Swift code using typeCGFloat. -
Allow the
CGFloatdeclarations to remain in imported Obj-C declarations. When passingCGFloatparameters, auto-convert bothFloatandDoublevalues to the architecture-dependent representation ofCGFloat. When receivingCGFloatreturn values, auto-convert to eitherFloatorDoublevia type inference. -
For edges cases such as arrays of
CGFloat, require the Swift-side representation to match the architecture-dependent representation (which may require#ifto get right, for multi-architecture code).
This solution relies on the fact (well, my claim) that passing Double values into Obj-C APIs that use 32-bit CGFloat requires conversion anyway, and that the semantics of Obj-C APIs that use 32-bit CGFloat is typically tolerant of the loss of precision. (For example, view coordinates are basically small integers, or halves or thirds of integers sometimes, which just happen to be passed as floating point. Extreme precision is pointless for this.)
Unless Iâm missing something important, this enables the following 3 cases:
-
A body of code only for architectures where
DoubleandCGFloatare interchangeable, that doesnât ever mentionCGFloatand has no explicit conversions. Nothing can go wrong.
-
A body of code that carefully manages values and expressions as
FloatorDoubleon a case-by-case basis, passing some of those results to Obj-C APIs with no explicit conversions and negligible loss of precision at the API boundary. Very little can go wrong.
-
A body of code that introduces its own
typealiastoDoubleorFloataccording to architecture, that has values and expressions that look the same but use a different representation per architecture â that basically reintroduces its ownCGFloatlookalike, when that approach is deemed best for that body of code. This is more or less the status quo ante.
My suggestion is that we go a bit deeper into magical territory:
- Force the compiler to treat
CGFloatas an uninstantiable type in Swift. That means no explicit invocations of aCGFloatinitializer, and no properties/variables declared in Swift code using typeCGFloat.
Unless I'm misunderstanding, I believe this would invalidate a huge amount of codeâin the app codebase I most frequently work on, this would require changes to almost every file of UI code due to constants defined explicitly as CGFloat.
Overall, the implicit conversion approach satisfies me. It's narrowly targeted and would be a huge QoL improvement when working with UIKit and other CGFloat APIs.
I think concerns about introducing this special case conversion are being overblown. The implication I've gotten from this thread is that (although it is outside the scope of Swift evolution) it is the plan-of-record to use this conversion to phase out the use of CGFloat in Swift APIs, preferring Double in all cases. With this in mind, I see no compelling reason to complicate this feature further in order to have a more pleasing abstraction.
I believe this would invalidate a huge amount of code
I'm not sure it's fair to throw reality in my face when I'm being magical! ![]()
In reality, I could imagine modifying the details of my suggested approach:
-
In the importer, rewrite the
CGFloattype to something like@_cgfloat Floator@_cgfloat Double, according to architecture. -
Introduce an interim
typealiasofCGFloatto eitherFloatorDouble, according to architecture.
That probably doesn't solve every case of source breakage, but I think it would basically turn existing code into my case #3.
The implication I've gotten from this thread is that (although it is outside the scope of Swift evolution) it is the plan-of-record to use this conversion to phase out the use of
CGFloatin Swift APIs, preferringDoublein all cases.
In that case, you're necessarily going to have to get rid of the CGFloats at some point, just not immediately.
I'm not sure it's fair to throw reality in my face when I'm being magical!
Sorry to burst your bubble. ![]()
In reality, I could imagine modifying the details of my suggested approach:
- In the importer, rewrite the
CGFloattype to something like@_cgfloat Floator@_cgfloat Double, according to architecture.- Introduce an interim
typealiasofCGFloatto eitherFloatorDouble, according to architecture.
IMO, this steps into the realm of what I was worrying about above, where we overcomplicate the feature in the name of ideological purity to avoid the "implicit conversion" bogeyman. To the extent that the project of phasing out CGFloat is successful, any implementation approach will eventually become a footnote.
To me, "there's an implicit conversion between Double and CGFloat" is a much preferable vestigial feature than a more complex solution based on underscored attributes, property wrappers, Objective-C importer changes, or whatever else, especially given the precedent in the language for these sorts of conversions for Objective-C compatibility.
In that case, you're necessarily going to have to get rid of the
CGFloatsat some point, just not immediately.
I don't followâit would only be necessary if this conversion didn't exist. With the conversion, I could happily start writing new code using Double and never touch the old CGFloat-based code again.
To me, "there's an implicit conversion between
DoubleandCGFloat" is a much preferable vestigial feature than a mess of attributes, property wrappers, Objective-C importer changes, or whatever else, especially given the precedent in the language for these sorts of conversions for Objective-C compatibility.
(To be exact, no one's proposing "a mess" of multiple features. There's a mess of features that sundry individuals are proposing one of, just not the same one.)
My point (my original point, not my proposed solution) is that there's no valid solution involving automatic conversions for the general case of Swift code that mixes Float and Double values, whether for multiple architectures or not, whether via CGFloat or not.
For subtle reasons (I argue), such code needs to control when and where conversions happen, with explicit casts. Without such controls, code is going to be very hard to debug, and very hard to verify.
What I'm proposing, stripped of potential implementation details, is that "an implicit conversion between Double and CGFloat" is fine if it's limited to API boundaries. The limitation might be a bit messy to state correctly, but that solution is really no messier than your preferred solution, and somewhat safer.
(To be exact, no one's proposing "a mess" of multiple features. There's a mess of features that sundry individuals are proposing one of , just not the same one.)
Sorry, that was... uncharitable of me. I've edited that portion.
My point (my original point, not my proposed solution) is that there's no valid solution involving automatic conversions for the general case of Swift code that mixes
FloatandDoublevalues, whether for multiple architectures or not, whether viaCGFloator not.
Sure, if 'valid' means 'no implicit loss of precision', but I think this portion of your original post summarizes nicely what I expect to apply the vast majority of cases:
(For example, view coordinates are basically small integers, or halves or thirds of integers sometimes, which just happen to be passed as floating point. Extreme precision is pointless for this.)
For any use of CoreGraphics/UIKit API, the loss of precision is a non-issue because of pixel rounding. For any existing code, precision loss must already be handled explicitly somehow. For new code, we always prefer Double to CGFloat, so CGFloat values will be eagerly converted to Double unless the user specifies otherwise.
So, the way I see it, there's a only very specific set of circumstances that can cause issues. A user must:
- Write new code that traffics in both
DoubleandCGFloat. - Explicitly type some intermediate value as
CGFloatwhen they don't intend to lose precision. - Pass the ultimate result back to a high-precision
Double-typed parameter.
This strikes me as... unlikely? This conversion would remove the need to ever explicitly type something as CGFloat, so it should be immediately obvious that something is 'wrong' if someone has written it out in source. It would be easy for a linter rule to catch for anyone who did prefer a "disallow CGFloat entirely" approach like you.
So, the way I see it, there's a only very specific set of circumstances that can cause issues. A user must:
- Write new code that traffics in both
DoubleandCGFloat.- Explicitly type some intermediate value as
CGFloatwhen they don't intend to lose precision.- Pass the ultimate result back to a high-precision
Double-typed parameter.
I think @QuinceyMorrisâs suggestion is intriguing to consider here: if we restricted the proposed convenience (or, perhaps, specifically the implicit narrowing) to applying only at API boundariesâthat is, when calling functions declared in another module and converting return values of type CGFloat from such functionsâthen we avoid this scenario entirely while preserving the raison dâĂȘtre of this proposal.
I think @QuinceyMorrisâs suggestion is intriguing to consider here: if we restricted the proposed convenience (or, perhaps, specifically the implicit narrowing) to applying only at API boundariesâthat is, when calling functions declared in another module and converting return values of type
CGFloatfrom such functionsâthen we avoid this scenario entirely while preserving the raison dâĂȘtre of this proposal.
I had originally hoped for something like this, but it's not a tidy as one would like it to be, both from a practical type checker implementation perspective, and because type checking happens at expression scope, not block scope. So this would work:
takesCGFloat(returnsCGFloat() * someDouble) // OK
but this would not:
let a = returnsCGFloat() // CGFloat
takesCGFloat(a * someDouble) // error, CGFloat * Double
I can imagine sets of rules that get one out of this box, but they get messy pretty quickly. Pavel and Holly explored some of them for me a while back, and ultimately settled on the current direction because it's just much more practical.
Also, + and * are "functions declared in another module". Are they included in this new "convert only at API boundaries" rule? Or do they get special exemptions?
And if a user has a pre-existing function of their own that traffic in CGFloat in their module... they don't get the conversion? But their functions in other modules do?