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

so far, it seems very unclear to me whether the goal of the section of the community interested in this sort of change is to resolve specific frictions in certain locations (e.g., C imports), or pursue more far-reaching changes to the type system. is the problem with using CGFloat, or is the problem CGFloat itself?

1 Like

To me, the problem is really in having two types being declared in different modules that in practice conicide and could be supposed to mean the same, given one of them declares an initializer from the other one. CGFloat in Core Graphics, Double in the Standard Library — but technically serve the same purpose. Another example I mentioned in the pitch thread is the NSRangeRange<Int> pair — they are declared in Foundation and Stdlib, respectively, while their semantics (if I'm not missing something) fully coincide too. The pain point is when I, importing the two libraries, see that these things really do the same thing and am sure it's meant to be so, but because the types were declared independently, all I can do is to constantly convert them back and forth even if their memory layout is the same.

The goal of this proposal is to improve the ergonomics when working with CGFloat. The problem is that CGFloat causes code not to type check for no good reason other than it's a separate type from the standard type developers use for these kind of computations, and it's incredibly frustrating, regardless of whether you're calling a CoreGraphics API or doing some computation with a CGFloat member of your own type. Of course, there are a lot of different opinions on how exactly to achieve that goal.

I personally am not interested in implicit conversions only for certain declarations. When it comes to type conversions, I strongly believe that conversions should behave the same way no matter where you write the code.

Also keep in mind that "consensus" isn't really a goal of evolution reviews, as I understand it. These are all ideas put forth for the Core Team to consider and make a decision. It's perfectly okay for different people to have different opinions about how to solve a given problem.

12 Likes

+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.

6 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.

9 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.`

10 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 think we (people who are negative on the proposal) all understand the goal. The questions we are ultimately raising are those of consistency, the potential mental cost of learning the compiler quirk (also let's be honest here, we all are not in a beginner's shoes when it comes to Swift at least, so all our judgement is potentially skewed) and whether this feature will generally become something that is rather resented in distant future:

— the problem is that by accepting such compiler intrinsic, we are also propagating this quirk of history into a newer language that we otherwise would like to keep as pristine as possible. My further, personal take on this is that all already existing conversions (the much frequently mentioned CF-NS ones, for instance) too were not the best decision when it comes to clarity, as I personally had to spend many hours researching all the legacy background behind these types to understand these conversion patterns and reasons behind them and remember the list of types that undergo such conversions. They were perhaps toll-free bridged for the CPU, but definitely not for my mind.

— so what I imagine now is that if I were new to Swift after the proposed change has been accepted, I would need to read all these two long threads to grasp what's going on.

Furthermore, I personally don't even find the CGFloatDouble case that disturbing. Right, it's inconvenient, but after some time I got used to this and I don't even notice writing the initializers in the right place, and that's the price I'm wholeheartedly willing to pay for clarity and consistency. On the other hand, because this change relaxes the type safety of the language, I'm actually starting to have the urge to think about how I could circumvent this change and ensure that my CGFloats stay CGFloats and my Doubles stay Doubles.

This summarises my thought even more: if I have to type Int32(someInt) + someInt32, I can't grasp why floating-point types are any different on the type system level (except, again, being unfortunately pervasive throughout Apple's graphics API).

5 Likes

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

"We" as in "the reviewers" suggesting on whether to accept the proposal or not.

To me, being more or less pristine aligns with usability more than it doesn't — because in purely that regard, weakly typed languages are more "practical": you know all the conversions, you type less. Clearly, that's not the path Swift has generally chosen, so any discrepancy is actually more detrimental to me than it could have been if Swift were more forgiving of the type safety overall. Or that might again just be me, and my understanding of what's practical misaligns with the broader consensus of it.

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
Terms of Service

Privacy Policy

Cookie Policy