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

+1. Not much to add to what is discussed. I just hope we move towards keeping Apple platforms affordances / integrations cleanly separated from the core compiler.

I agree, but this happens with other types as well. Data in Foundation and ByteBuffer (and other similar types) for example (although the latter is less common, I’ve been having difficulties getting into server side Swift due to these sorts of differences).

Personally, I would prefer a more generalized case of this pattern, but if CGFloat/Double could later be “converted” to this more generalized implementation, then I’m +1 on this proposal, as I think it could be a good step in the right direction. I do think caution is warranted though, as it could head in the direction of creating too much invisible, “magical” syntactical sugar that obfuscates what’s actually happening.

5 Likes

Yes, from the implementation perspective this work is definitely generalizable, but I think it might only be worth to consider widening implicit conversions and even that would require a lot more analysis how it fits into the language and what performance and type-checking implications that is going to have.

2 Likes

I am negative on this proposal. It is well written and is probably the best solution to the problem available, but I don't think it's a problem that should be solved in the Swift language.

It seems strange to privilege one particular type, from one particular API. It would be better to come up with a general mechanism, so e.g. other types could be integrated into the language. This could help not just with particular APIs but integrating with other languages which have similar 'float' types.

But I don't think even then it would make sense. One of Swift's best features is its type safety. This leads not only to safer code but enables strong type inference which leads to more readable, maintainable code. Adding automatic conversion goes against this.

The proposal does address this, with detailed rules that ensure nothing untoward happens. But that means developers need to learn, or least be aware, of all these rules, rules much like the complex type conversion/promotion rules seen in other languages. I don't think they belong in Swift.

8 Likes

+1

There's perfectionist versus practicality at play here. It would be nice if we could design something that is part of the language, but the simple fact is CGFloat(someDouble) is a common annoyance for iOS developers (which still makes up of the majority of Swift's use). I think the addition of special-cased language rules is worth it. Hopefully, these rules will allow Apple SDKs to gradually migrate away from using CGFloat everywhere and then the special-casing can — in the long arc of time — be deprecated.

This proposal doesn't preclude an implicit conversion system from being developed in the future, even if the behaviour of CGFloat remained 'magical'.

14 Likes

I think the core of what I'm trying to say is that all developers, including complete newcomers, need to learn something about working with CGFloat. There is no way to completely eliminate that. So, for me, the most simple, straightforward story of how to work with CGFloat would be the most teachable and easy to remember.

I'm starting with the notion that a complete newcomer would typically learn about Double relatively early in their learning path, and learn that it is the default and preferred floating point type in Swift. The developer then decides to learn some graphics APIs and encounters CGFloat.

The current story is something like "Even though Double is the preferred Swift floating point type, graphics APIs use CGFloat. You need to explicitly convert between Double and CGFloat. If you do your calculations in Double, as you do everywhere else in Swift, you can convert when you call an API that takes a CGFloat. But if your calculation uses a CGFloat, like getting the width of a CGRect, then you need to convert it to Double to use it in your calculation. Or, if you are doing a lot of calculations that use CGFloat, you can do all of your calculations with CGFloat by explicitly declaring your variables and constants as CGFloat instead of the default Double. Sometimes you'll find your code has less conversions and explicit types by working in Double, sometimes by working in CGFloat."

To me, that's a lot to understand especially for a complete newcomer.

With the proposed change, the story becomes something like: "Even though Double is the preferred Swift floating point type, graphics APIs use CGFloat. You can do all your calculations in Double as you do everywhere else in Swift and anywhere you see CGFloat you can treat it as being the same as Double."

I think the proposed change makes the rules of how to deal with CGFloat and what a complete newcomer needs to learn and remember much more simple than what is currently in place.

4 Likes

Not all. I never had to, it's not used anywhere in my app which uses Metal. Other apps will be similar. Larger apps might have developers working on different aspects only some of whom are dealing with graphics APIs. Or an app might not have a UI, or it might run on a non-Apple OS.

I think generally you are correct about the benefits of type safety. In this case though (as with the existing automatic conversion between CF and NS types) the explicit conversions don’t provide a benefit in guarding against incorrect code.

On 64-bit platforms, the conversion is between two identical representations of a floating point value, so there is no degree of safety added.

On 32-bit CGFloat platforms (watchOS) the safety would be in explicitly noting the narrowing from Double. But initializers for CGPoint, CGSize, and CGRect that accept Double have already been silently doing this narrowing for at least five years now without causing widespread issues. So, there is not any practical safety gained in the 32-bit CGFloat case either.

I also believe that working in CGFloat is experienced as a pain point for many developers specifically because the resulting code is less readable and maintainable. So in this case this other typical benefit of type safety is not present.

It seems you are saying that the proposal mitigates any safety concerns to your satisfaction, but it is the complexity of adding new rules that is the real issue.

I think there is typically a significant difference between what a developer needs to know and understand to be able to write code successfully, and what a developer would need to know to fully understand all of the underlying details.

For example, there are certainly many developers successfully writing SwiftUI code who do not fully understand all of the rules and details of how property wrappers or result builders work.

In this case, I think the rules to learn how to successfully use APIs with CGFloat are significantly less complex with this proposal than the current state of things, as I detailed in a previous post.

And yes, of course, if you never use a framework that uses CGFloat you never have learn how to work with it.

I think this illustrates that this proposal is unlike pervasive complex type conversion and promotion rules seen in other languages. You could potentially spend years writing Swift code without ever encountering CGFloat, in which case you never need to know anything about it.

6 Likes

+1 I'm strongly in favor of this proposal. It would greatly benefit the usability of Swift for the development of Apps on Apple platforms, which I guess was the main reason to develop Swift in the first place.

I had a quick read of the proposal.

I'm a -1 on this. Swift has so far been pretty principled with its stance of explicitness, and when that has changed, it has been done so in a more general manner.

I am highly doubtful and concerned that this is a stop-gap measure until Apple's APIs evolve to use Double explicitly, or that reality will ever exist.

This will be a new rule on how types behave in the system that is special-cased to only Apple's APIs.

Double and CGFloat are no different or special than any other types that also need explicit conversions.

In my own iOS codebase there are several places where I have two types that have identical fields, but for various reasons need to be two explicitly different types - I convert between the two quite often. Why is CGFloat and Double more important to provide implicit conversion than my other types?

That's a pretty big "hopeful".

In my experience, when something comes along to fix pain points like this... it lets the pain points persist longer.

Being a constant feedback that "It's painful to work with your APIs" is much more of a motivator to start designing APIs differently than "This feature is 'deprecated' and I should be writing more 'correct' code".

The most permanent changes I've seen in my codebases have been "temporary" solutions.

The other way to teach is:

"Double is the preferred and default representation Swift floating point type. Apple's graphics APIs define their own floating type called CGFloat. When you are doing calculations of mixed between these types, either you need to convert all your Double values to CGFloat or convert all your CGFloat values to Double, just as you would any other arithmetic types in Swift.`

9 Likes

It isn't more important. The goal here isn't to provide implicit conversions, the goal is to eliminate a pestilential type (CGFloat) from Swift code. It just happens that for … reasons … a narrowly-focused implicit conversion has been chosen as the means to that goal.

If you ran into a similar situation with your "other types", you'd have the option of removing their declarations from your code, and adjusting the usage sites to avoid them. That strategy isn't available for CGFloat, because … reasons.

(The "reasons" are why we have 2 long threads about this.)

4 Likes

My argument for this is that the distinction between CGFloat and Double is a quirk of history rather than an intentional decision. We should never support implicit conversion of arbitrary nominal types (as e.g. the difference between a Point { x: Double, y: Double }, MapCoordinate { x: Double, y: Double } and Offset { x: Double, y: Double } is significant), but Double and CGFloat essentially have identical meanings. There's no semantic distinction here. With some minor handwaving on technicalities, if CGFloat could be redefined as a typealias of Double, that would be ideal and preferred. But that can't happen so this compiler magic is the next best thing.

6 Likes

Although I'm not sure whether this is an irrelevant opinion for this proposal's implementation, if this would make a regression, just like String — to make sure you have pure Swift.String but not implicitly converted NSString, you have to call String.makeContiguousUTF8() — I'm strongly -1.
(Not only runtime performance but worried about compiler type check slowness too.)

Otherwise, +1.
One benefit I like is you don't have to import CoreGraphics in a model file anymore (nor declare in Double then do explicit conversion in a file for UI), which always felt awkward to do so.

I don’t know the “we” that you are referring to in this sentence. Keep in mind that Swift, from the start, has described itself as a pragmatic language. The goal is to create a language that is excellent in practical usage and very much absolutely not to create a “pristine” language for its own sake.

5 Likes

The situation of ending up with a pervasive type you wish you could kill off is not unique to Apple.

It’s probably too late to bring this up, but in an alternate reality where the decision were mine and mine alone, I would probably implement something like “API wrappers” instead:

import CoreGraphics

public apiWrapper HideCGFloat {
  func wrap(_ outwardFacing: Double) -> CGFloat { CGFloat(outwardFacing) }
  func unwrap(_ inwardFacing: CGFloat) -> Double { Double(inwardFacing) }
}

// With that the compiler would synthesize roughly this:
extension CGPoint {
  @inlinable init(x: Double, y: Double) {
    self.init(x: HideCGFloat.wrap(x), y: HideCGFloat.wrap(y))
  }
  @inlinable var x: Double { HideCGFloat.unwrap(x as CGFloat) }
  @inlinable var y: Double { HideCGFloat.unwrap(y as CGFloat) }
}
// ...
// ...and it would do so for every piece of API, declared or imported.

With an apiWrapper feature like that, SwiftUI could solve all its problems by just including HideCGFloat in the next release.

But users could apply their own apiWrappers in other cases too:

/// Enables using file paths with URL‐based APIs.
internal apiWrapper FilePaths {
  func wrap(_ outwardFacing: String) -> URL { URL(fileURLWithPath: outwardFacing) }
  func unwrap(_ inwardFacing: URL) -> String { inwardFacing.path }
}
/// Allows supplying raw strings to APIs expecting rich text.
internal apiWrapper RichText {
  func wrap(_ outwardFacing: String) -> NSAttributedString {
    return NSAttributedString(string: outwardFacing)
  }
  // No “unwrap”; conversion only goes one‐way.
}

The weight of conversion cost vs convenience could then be decided either by the API author (with public and in the library) or by the client (with internal and in client code).

6 Likes

What is your evaluation of the proposal?

:+1:t3: I'm in favour of this proposal. It will improve the clarity and readability of code that I have in production now.

I understand there are concerns that something more generic should be designed to handle other cases like CGFloat, but I've not yet seen any other similarly widespread type wrappers in use in the ecosystem, so I feel this one is a special case.

Is the problem being addressed significant enough to warrant a change to Swift?

Yes. CGFloat to Double conversions exist in all of my code bases, and it shouldn't be necessary to continually convert between CGFloat-as-a-wrapper-for-Double and Double.

Does this proposal fit well with the feel and direction of Swift?

I feel it does, given Xiaodi's reminder that Swift was envisioned as a pragmatic language. This is a pragmatic change that will make things simpler in use for a great many developers on Apple's platforms using Swift today.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I haven't faced the same issue in other languages that I use.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I've read the proposal, and I've been following the threads here on the forums closely as the proposal developed.

4 Likes

-1 from me.

This proposal should be feedback to the CoreGraphics team:
"CGFloat is difficult to work with in Swift, please make it easier -thanks".
1 year later...
WWDC22 - Session 405: Getting started with CoreGraphics2
wow magic :stuck_out_tongue_winking_eye:

Others have suggested broader language improvements that make all imported conditional typealiases better to work with. I don't personally see how that could be done and be safe, but I'd be on board with a proposal like that if someone can figure it out. However this proposal just panders to Apple platforms without actually adding anything to the Swift language. I work with C/C++ libraries all day and have to cast constantly between scalars. That's just the way it is. As I joked above, I think this is a CoreGraphics problem not a Swift problem.

A better proposal that defines an automatic allowed cast between types would be really cool for working with C/C++. Could do a feature that fills the cast in when needed.
Maybe make it declared with imports and file scoped.

import CoreGraphics
autocast CGFloat as Double, Float32, Float16

The type system would choose the cast.
In general this is safety defeating and it would be abused because it's too easy; however it would make some C/C++ stuff much easier to handle.
Perhaps restricted the feature on a package level so package creators are the only ones that can choose to implement it. If the CoreGraphics team decided CGFloat can be implicitly cast to Double they could implement it in their shims. While people like me could make C/C++ packages that expose values of type Int and UInt instead of Int32 and UInt32.

But this proposal as is :-1:

1 Like
  • What is your evaluation of the proposal?

+1. CGFloat is an annoying aberration in my day to day development. Seeing SwiftUI switch to it was especially disappointing. Getting rid of most uses will be great.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes, given that's where the conversion layer lives for these imported APIs.

  • Does this proposal fit well with the feel and direction of Swift?

95%. I'm a big fan of Swift's no-implicit-conversion rule, but I find the proposal's argument that this simply treats CGFloat as an imported API with automatic conversions, as precedented by other types, compelling.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

No.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Read the proposal and the reviews in thread.

4 Likes

I'm -1 on this proposal as it stands. I want to be happy about the prospect of less boilerplate, but I can't ignore the tradeoffs.


My issue with implicit conversion, is that it makes the case that the two types are interchangeable, but stops short of actually declaring a preference for which one should be used in Swift going forward. Some devs may adopt CGFloat everywhere (documentation will not prevent this, only discourage it), while others will adopt Double.

This will create dialects of Swift, and blind people to potentially lossy conversions in their code. It will do this without actually solving the risk of lossy conversions (the risk increases if anything), or the confusion as to why we have two floating-point types in active use. It purely solves the problem of writing out initialisers in code. I don't think these tradeoffs are worth it for what is effectively ‘syntactic sugar’, and I think we can do better.


The proposal suggests we think of CGFloat as a ‘retroactive typealias’ for Double going forward, but the subtext is that we can all safely forget about the risks of lossy conversions. I suppose this must be taken as given, or else explicit initialisers are clearly the right choice.

Given Double and CGFloat are effectively interchangeable, my preference would be to map CGFloat to Double at the importer level, similar to how NSString is currently mapped to String. Implicit conversions may be a useful tool alongside this change, but simply for backwards-compatibility of source code. This would push users towards using Double universally (rather than interchangeably), eliminating most of the tradeoffs that come with the proposal as it stands.

(The proposal currently lists this as option 4 originally considered to solve this problem, and goes on to note the original reason this wasn't chosen has been shown to be a non-issue!)

3 Likes

It is my take, that we shouldn't worry too much about lossy conversion in the specific case of Double / CGFloat. This inconvenience is mostly affecting iOS and Mac developers who are already doing lossy conversion, just explicitly.

That 0.001% of developers that actually need to worry about lossy conversion will probably be experienced enough to understand that the conversion is happening.

3 Likes