SE-0307: Allow interchangeable use of CGFloat and Double types

The review of SE-0307, "Allow interchangeable use of CGFloat and Double types", begins now and runs through March 24, 2021.

The proposal is authored by @xedin.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager or direct message in the Swift forums).

What goes into a review of a proposal?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift.

When reviewing a proposal, here are some questions to consider:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

As always, thank you for your contribution to making Swift a better language.

Ted Kremenek
Review Manager

22 Likes

I fail to understand how the difference between CGFloat and Double can be relevant and irrelevant at the same time.

I understood why CGFloat existed in APIs inherited from Objective C, but because all Swift documentation recommended using Double instead, I assumed CGFloat was consigned to die off at the same pace as NSString and friends.

Then SwiftUI came out and surprised me by using CGFloat throughout the API. It looked so out of place. But it made me think CGFloat must actually have some forward value that I was unaware of. If the SwiftUI team is allowed to say, why did they make that decision? If CGFloat is just an outdated spelling of Double (albeit that had some neat quirks on obsolete architectures), then why didn’t SwiftUI just use Double across its API?

Since SwiftUI’s entire API heavily implies that there are times when you should use CGFloat instead of Double, it seems illogical to me to add a compiler feature whose sole purpose is making it easier to revert to the wrong one by accident.

Either SwiftUI did the wrong thing, or this proposal does the wrong thing, or I totally misunderstand CGFloat. Which is it?


CGFloat is a type that is defined by Apple frameworks, it makes this change specific to Apple platforms

It is also implemented in Foundation and will affect all platforms. Care must be taken that both CGFloats behave the same.

22 Likes

I am pleased with the final shape of this proposal. It certainly addresses a clear, practical shortcoming with a tailored and pragmatic fix. It fits well with the existing feel and direction of Swift in making such a pragmatic choice, and in particular the attention to minimizing the number of narrowing conversions will likely result in numerically superior results to what some users would have been able to come up with using manual conversions. Most other languages do not have to deal with this particular problem, but this fix is more targeted than the alternative of adopting C-like implicit conversions. I have spent time studying this proposal in depth.

I think it’s fair to have the writing clarified to note that CGFloat is also defined in swift-corelibs-foundation and therefore available on all supported platforms. In practice, though, I expect that the conversions noted here will impact watchOS (and it would be nice to clarify why that is for readers who may not be aware of this for the versions of Apple Watch with 64-bit processors).

One final nit in the writing would be that it would be nice to standardize the writing in terms of the arrows used in the text; I find the alternating emojis and ASCII renditions to be jarring.

8 Likes

This is a very clearly motivated proposal that will directly address a significant ergonomic problem with commonly used frameworks. I appreciate the narrow targeting of the design, which avoids broader scale erosion of the type system.

I read the proposal briefly but was not involved in the pitch phase. I am also not an expert on the type checker's constraint solver, so I cannot attest to the impact on compile times or ambiguities etc.

-Chris

3 Likes

Sorry about that, fixed!

1 Like

+1

The proposal addresses a common and frequent pain-point with a targeted solution that I think fits quite well, and I, for one, desperately want this to be available.

I followed the pitch to the proposal, but didn't participate myself - I can't speak to any of the internal aspects of what this will do.

3 Likes

This is really not what SwiftUI's API suggests. Rather it suggests that without a feature like this, it was impractical to ship an API on Darwin that has to interoperate with CoreGraphics and uses Double without introducing excessive noise in client code. Having this feature would allow the "next" SwiftUI (whatever that thing might be) to use Double.

7 Likes

It may not have been intended, but that is how it came across out in the wild. And it resulted in a lot of things switching away from Double to use CGFloat under the perception that “something” must be better about it. (Hypotheses included performance, future‐compatibility with 128‐bit, etc.)

If Double is the real recommendation, and this proposal helps consolidate things toward that direction, then I support it.

I am +1 for this proposal.

I think this proposal presents a focused solution that provides a significant ergonomic improvement when working with common frameworks that have CGFloat APIs, including Core Graphics, Core Animation, and SwiftUI.

The proposed change allows ‘naturally-written’ Swift code without type annotations to work with CGFloat APIs without manual type conversions. I believe this will encourage the use of the preferred Swift floating point type Double and significantly improve call site clarity. So, I think this proposal fits very well with the feel and direction of Swift.

I am not well versed with the internals of the type checker, but I trust those who are expert will chime in if this proposed change causes significant performance or other issues.

I followed and contributed to the pitch thread for this proposal.

32-bit CGFloat
One potential issue is that on platforms with 32-bit CGFloat values, such as watchOS, there is a loss of precision when narrowing from Double to CGFloat.

In practice, I don’t believe this will be a significant issue, since APIs that use CGFloat must already be suitable for use with 32-bit float values.

In addition, the Core Graphics overlay for Swift introduced five or six years ago adds initializers for CGPoint, CGSize, and CGRect that take Double values.

On platforms with 32-bit CGFloat, these initializers have been doing a narrowing conversion for a half a decade or so, and have not seemed to cause any widespread issues.

Finally, it is generally better to work in the higher precision type and only lose precision when necessary. This proposal enables doing so without effort by the developer:

  • Declared constants and variables are Double by default.
  • CGFloat values pulled from structs like CGPoint or CGSize are automatically promoted to Double when used in a calculations with Double.
  • Passing resulting Double values into API that takes CGFloat performs the narrowing.

So, the most straightforward way of writing the code also leads to all intermediate calculations using Double, with narrowing only happening when API calls require it.

That all said, I do think it would be beneficial to add more detail in the proposal about the considerations regarding 32-bit CGFloat, especially since evolution proposals also serve as a way for developers to understand the change in the future.

Less CGFloat
One effect of this proposal, I think, is that it will greatly reduce or eliminate the need for declaring variables of type CGFloat in client code. All calculations can be done in Double. In fact, CGFloat variables become a bit of a red flag that unnecessary or unintentional narrowing may be happening on 32-bit platforms.

I hope this proposal is approved. I think it will make code clearer, easier for newcomers, and more enjoyable to write.

12 Likes

i’m strongly –1 on this proposal.

this is a major change to the semantics of the language, and the fact that it only applies to one pair of types doesn’t seem like a solid justification for characterizing it as a “narrowly-tailored” solution. if we break a fundamental assumption in the language, then that assumption is now broken, even if it was only broken once.

the real problem here, which doesn’t seem to have been mentioned in the Alternatives Considered section, is that we don’t have a floating point type with platform-defined width, the way Int and UInt are currently related to Int32, Int64, and UInt32, UInt64.

note that this is not the option 3 considered in the proposal, which is to have a platform-dependent typealias CGFloat pointing to either Float or Double. the correct long-term solution would be to introduce a word-sized Real type (or some other, more appropriate name) that requires an explicit conversion to a fixed-width floating point type, just as Int requires an explicit conversion to Int32 or Int64. we should then encourage relevant APIs to be encouraged to be written using the word-sized Real type instead of the fixed-width types, exactly the way integer types are treated right now.

i understand the ergonomic motivations of this proposal, but i want to highlight that, in the past, this was not considered adequate justification for this sort of change to the language. for example, the codepoint and character literals proposal i put forth last year was a solution proposed to solve what was essentially the same problem in a different context — type ergonomics for interop with a certain class of C APIs — with very similar implications for the language type system. despite the fact that that proposal was limited to the type checker/inferencer, with no changes to actual program behavior (unlike this proposal), the codepoint literals proposal was rejected because the community felt that type ergonomics for the purposes of interacting with a specific class of C APIs did not warrant the proposed changes to the language.

i do not feel that there is a meaningful difference between the C APIs that this proposal aims to make easier to use, and the ones that previously rejected proposals aimed to make easier to use, other than that the APIs in this situation are vended by Apple.

19 Likes

I'm also negative on the proposal, and I will try to again make my point that I apparently failed to convey during the pitch phase.

I feel that the inconsistency introduced to the language is more problematic than the way it's being presented, and I would even argue that the aim to make this change seem as a narrow one that "just" extends the family of existing conversions actually speaks against the proposal, not for it, because it either fails to recognise that CGFloat is most likely not where this precedent will end — although there are such claims in the text, I don't believe it would stop people from looking for other "useful" conversions later on — or it fails to recognise that the problem is not even CGFloat per se, but generally any APIs and conventions that are clunky to directly use from Swift.

Even more, the latter issue doesn't align with the former: as Swift tries to expand its presence, there will be more and more discoveries of platform- or language-specific types that will be extremely unwieldy to use in Swift, so if we assume that CGFloat is where the train stops, then we undercut Swift's potential to be used in such contexts, and people would reach for other languages with better interoperability. On the other hand, if accepted, a precedent like this provides yet another anchor for discussions "if this conversion is as significant as the CGFloat one" (which is also the case here: the proposal partially relies on the fact that CF*NS* conversions exist), so I see a lot of potential for creating a battleground where each new interoperability effort would need to argue if the ergonomics enhancement is as major as in those former cases.

Other issues are those of discoverability/teachability and the potential to spawn language dialects. I would be quite blatant to say that the teachability of the language decreases dramatically with regards to such fundamental types, as I assume that all existing documentation will still list CGFloat, but here people, when encountering some code that relies on this conversion, would need to make additional effort to understand why it's allowed (which is the same problem as with CF*NS* conversions). One reason we, programmers, decide to use a more modern (and thus hopefully more consistently designed) language is simply in not having a need to have a cheat sheet alongside the keyboard to consult for all such quirks. As for language dialects, I don't think that this change reduces the potential of creating those: I can imagine codebases forbid the use of this conversion and require an explicit spelling of initializers — much like some codebases already disallowing the use of implicitly unwrapped optionals on a linter level.

Overall, I wish that the problem of type conversions will be considered much more holistically and we arrive on a means to make it much less ad-hoc. I formerly suggested that a keyword would be introduced for this purpose, which would make it scoped to the codebase or even just a function body that prefers (or not) make use of such conversions. Something like this will exterminate the need to ever debate whether some conversion is strong enough of a case and dramatically increase Swift's potential to interoperate with more and more languages and legacy APIs.

7 Likes

I think it's fair to imagine that this proposal could be used as a precedent and as both a weapon and a shield, depending on underlying intentions. I personally think it's okay to discuss other conversions in the future and their viability/merit should be considered from different angles on case-per-case basis.

This proposal represents a judgement call based on all of the feedback received from the users that have to deal with graphics APIs. I and others believe that this is the most narrow targeted solution we could provide here to alleviate common pain point in cross framework API use, which is also the most teachable one - CGFloat is a Double and vice versa; and doesn't have language dialect consequences like a new syntax general use would.

3 Likes

I agree that there is a genuine issue, but having a hypothetical Real type wouldn't, on its own, actually solve this problem.

That's because you'd still have to do conversions from whatever the types actually are at the API boundaries, such as Apple APIs that currently use CGFloat.

At a minimum, you'd also need to add to the Obj-C importer the ability to rewrite the API types from CGFloat to Real — assuming that the rules for the precision actually align between the two types.

However, there are still other API boundaries in existing Swift code that the importer wouldn't solve.

I think the problem we're really trying to solve here is the very narrow one of just eliminating CGFloat from Swift, using a big enough hammer to get the job done.

(The current solution doesn't literally eliminate CGFloat, but it makes it possible never to mention it in Swift code, mostly. That's nearly as good.)

6 Likes

if the problem lives at the API boundary, then that is where the solution ought to be applied. i agree that there is a gap in the language here, but it also seems to me that this gap is shaped like a custom attribute. since we’ve already come to an agreement (in the past couple of years) that custom attributes are connected to types, perhaps a better solution would just be to add some annotations to the cumbersome APIs:

// CGFloat API 

func mix(_ a:@Double CGFloat, _ b:@Double CGFloat, t:@Double CGFloat) 
    -> @Double CGFloat
{
    ...
}
// swift standard library 
extension Double 
{
    @implicitConversion 
    func withCGFloatValue<R>(_ body:(CGFloat) throws -> R) rethrows -> R 
    {
        try body(CGFloat.init(self))
    }
}

the annotations would be a far more precisely-tailored solution to the problem, because they would only perform expansions at the call site, much like result builders currently do.

this would also be way more extensible, because we could also store the implementations in protocols:

// CGFloat API 

func mix(
    _ a:@CGFloatConvertible CGFloat, 
    _ b:@CGFloatConvertible CGFloat, 
      t:@CGFloatConvertible CGFloat) 
    -> @CGFloatConvertible CGFloat
{
    ...
}
// swift standard library 
protocol CGFloatConvertible 
{
    @implicitConversion 
    func withCGFloatValue<R>(_ body:(CGFloat) throws -> R) rethrows -> R 
}
extension Double:CGFloatConvertible 
{
    // no `@implicitConversion` annotation required, 
    // since this is a protocol requirement 
    func withCGFloatValue<R>(_ body:(CGFloat) throws -> R) rethrows -> R 
    {
        try body(CGFloat.init(self))
    }
}

i honestly do not foresee the currently-proposed solution as settling the issue of missing implicit conversions, nor do i think any of the authors are even claiming this. using custom attributes instead changing language behavior globally is much more scalable.

for example, i think everyone can agree that any API that takes a String argument, should also take a Character. using custom attributes would allow us to express this idea as:

protocol ImplicitStringConvertible 
{
    @implicitConversion 
    func withStringValue<R>(_ body:(String) throws -> R) rethrows -> R 
}

extension Character:ImplicitStringConvertible 
{
    func withStringValue<R>(_ body:(String) throws -> R) rethrows -> R 
    {
        try body(String.init(self))
    }
}

func prepend(prefix:@ImplicitStringConvertible String, to string:String) 
    -> String

let x:Character = "@"
let y:String    = "implicitConversion"

prepend(prefix: x, to: y) // should return '@implicitConversion'

It’s worse than that; a hypothetical word-sized stdlib Real type propagates the problem to other platforms where it doesn’t currently exist. Why would we want to do that?

10 Likes

Today, all custom attributes are declaration attributes, not type attributes, meaning they don't impact the type of the declaration they're attached to (and they can only be applied to declarations).

What you're describing here is most similar to a parameter wrapper (SE-0293), and implicit conversions are not a good use case for that feature. For one, implicit conversions don't really make sense on a per-declaration basis. The argument here is that CGFloat and Double are ultimately meant to be the same type, so it wouldn't make sense for some APIs to support the implicit conversion and not others.

10 Likes
Off Topic

I guess it was true until two days ago (Custom type attributes by DougGregor · Pull Request #36458 · apple/swift · GitHub) :slight_smile:

3 Likes

by “connected to types” i meant the name of the attribute comes from the name of a user-defined type elsewhere. it should not impact the type of the argument, only how it is supplied by the caller.

I would make the case that the proposed change greatly improves teachability for newcomers since “You can use Double and CGFloat interchangeably.” and “Just use Double like everywhere else in Swift.” seem to sum up what someone new needs to know and understand.

I also think it’s important to note that CGFloat is almost always used alongside types like CGPoint, CGSize, and CGRect.

For someone new to Swift and new to using a framework with CG types, I think there's currently a fair amount of cognitive effort and language knowledge required to understand why in the code below it’s sometimes okay to use Double with CG calls and sometimes it’s not okay and requires a conversion:

let elapsedTime = Date().timeIntervalSince(startTime)
let progress = min(elapsedTime / duration, 1.0)

let height = 200.0
let midY = height / 2.0
let width = 450.0
let midX = height / 2.0
        
let center = CGPoint(x: midX, y: midY)
path.move(to: center)
path.addLine(to: CGPoint(x: center.x, y: 0))
        
let adjustment = .pi / 2.0
path.addRelativeArc(center: center, radius: rect.width / 2.0, 
                        startAngle: CGFloat(0.0 - adjustment), 
                        delta: CGFloat(2.0 * .pi * progress))
path.closeSubpath()

With the proposed change, usage can be explained in the statement “You can always use Double with CG types” which I think is easy to convey, understand, and remember.

EDIT: Edited code example for clarity.

I see this change as neutral with regards to risk of causing language dialects. I don't think there will be much interest in insisting on the explicit conversions because in this case I don't think that would end up having a benefit in terms of helping the code be correct.

Without a more fleshed out pitch, it's difficult to evaluate your suggestion fully, so I don't want to dismiss it out of hand. But my initial thought is that something along these lines would actively encourage language dialects. Allowing each codebase or module to define its own set of allowed implicit type conversions is essentially enabling the creation of dialects.

5 Likes

I am speaking from the perspective of a person who has been programming for just around two years, and I still quite freshly remember how I went through learning Swift. It's easy to teach to an experienced programmer who knows some other languages, sure, but any exclusive behaviour of a language is extremely difficult to internalise for a complete newcomer, because "just" stating the rule “You can use Double and CGFloat interchangeably” doesn't make them familiar with any "why"-s of it and just adds to their already ever-expanding list of things they "just" have to memorise. Double is just too fundamental of a type, so it can't be expected that they learn about this conversion as they would learn some more advanced conversion-like feature akin "same-module declarations are implicitly @inlinable" when the time comes; it strikes them basically from the day one. With such things, it just takes too long until it finally "clicks".

But I can't disagree that improving the ergonomics of CGFloat per se is an extremely good idea. That is, I'm not disclaiming any of this:

— I simply dislike the approach. What I wanted to point at (teachability aside) is that cherry-picking such potential conversions is very likely just not sustainable. It requires both a long discussion and an intrusion into the compiler each time, which just piles up inconsistencies in the language and complexity in the compiler code and generally slows down any interoperability effort. I'm not particularly pushing the idea with the keyword even, I'm just wishing for an opportunity to step back and look if there could be a more general solution to make it easier to work with all ponentially "inconvenient" types, because it would be sad to see Swift being stubborn with some library or a platform simply because a particular conversion wasn't deemed to be worth the effort of implementing.

I'm optimistic that there could be ways to restrict such a feature from being abused and spawning language dialects — for instance, by making it extremely narrowly scoped, possibly just for a function body and only for external calls. For example, consider:

import CoreGraphics

// declared in my module
func foo(_ x: CGFloat) { ... } 

func myFunction() {
    // the keyword syntax, scoped to `myFunction`:
    conversion CGFloat: Double

    // OK, compiles, makes an external call
    path.addRelativeArc(center: center, radius: rect.width / 2.0, 
                        startAngle: 0.0 - adjustment, 
                        delta: 2.0 * .pi * progress)

    // ERROR: `conversion CGFloat: Double` can only be used for external calls, use `CGFloat(_:)` initializer
    foo(someDouble)
}

— again, that's just an idea, there could be better ways to design it, but having a similar mechanism would improve the ergonomics of potentially everything.

3 Likes
Terms of Service

Privacy Policy

Cookie Policy