How to use Foundation Measurements in Swift 6

Foundation has a Measurement struct (its "native" version of the Objective-C Foundation's NSMeasurement), which is conceptually just a Double and a 'Unit' which while formally declared as a class is effectively nothing more than an unbounded set of enum values.

In reality these are all immutable, so they're implicitly thread-safe. They are not, however, actually marked as Sendable.

Is there any real way to use them with concurrency in Swift 6, without having to basically turn off all the safety mechanisms (e.g. nonisolated(unsafe), @unchecked Sendable, etc)?

(this is not the only problematic part of Foundation, let-alone other Apple frameworks, but I figure it's as good an example as any to start with)

2 Likes

I'd really like a general statement from Apple here. Are we supposed to be reporting these things as bugs? Is this it as far as Sendable adoption goes? Is it just in progress? There is still so much non-Sendable stuff in Apple's frameworks I'm honestly kind of shocked.

5 Likes

Even in "new" frameworks like SwiftData there are Sendable conformances missing (FB13862584) — SwiftData SchemaMigrationPlan and … | Apple Developer Forums

I assume there's still a lot in progress, so reporting them via Feedback Assistant is probably the best bet.

1 Like

Hey @wadetregaskis - thanks for bringing this up! Are you able to share some sample code that demonstrates the issue? In particular, I believe all of our concrete Unit subclasses in Foundation should be marked as Sendable (for example you can see the documentation of UnitLength here does list Sendable as a conformance). However, we left the superclasses like Unit and Dimension as non-Sendable since it's possible for a client to make a non-Sendable subclass. I'm hoping some sample code can help illuminate the issue of why the conformances on the subclasses aren't working as expected, but we'd appreciate any feedback assistant submissions / issues on the swift-foundation repo for any code there that you find that doesn't work well with Swift 6 concurrency features!

1 Like

Hey Jeremy, I appreciate the reply.

The issue in my case is that I'm writing generic code & types to work with Measurement and therefore Unit; I can't restrict it (neither technically nor for my needs) to a subset of units.

Nonetheless, I even tried various approaches involving things like Unit & Sendable, but I couldn't get it to work. (I might try again later - it was complicated by a bajillion other Swift 6-related errors, which I've mostly now addressed, so maybe I'll have more luck now.)

For now I've just papered over the issue with rampant use of @unchecked Sendable on containing types. It's horrible but it seemed to be the only path forward.

I fear I'll ultimately have to rewrite Measurement with my own version based on simpler value types. Which isn't hard per se, it's just a bummer to have to explore such avenues. I assume what's in Foundation is never going to change in that manner, due to backward-compatibility requirements.


Tangentially, this smells like a broader issue relating to classes and Sendable. The compiler won't even let us declare a class Sendable unless it's final, outright forbidding valid designs where everything is simply Sendable.

It's not at all clear to me why I can't declare a base class Sendable and that simply means all subclasses must also be Sendable. That's how it works for every comparable protocol (e.g. Codable).

Sendable isn’t a protocol.

:man_shrugging: Semantics, really. It mostly behaves like one. More importantly, it makes sense to be able to say "this class and all its subclasses are sendable".

I assume the current lack of support for this is more about some technical detail or implementation difficulty, than principle?

No, it is a very explicit and deliberate design decision. You can use @unchecked Sendable on the base class to allow inheritance.

I took another look at it this, and actually it does seem like I can almost work with Unit & Sendable, except I need custom units, and it appears to be impossible to create Sendable custom units.

public final class IncidentEnergy: Unit, Sendable {
    …
}

:x: 'Sendable' class 'IncidentEnergy' cannot inherit from another class other than 'NSObject'


And, 'til now, I've just been creating them through the convenience initialiser, e.g.:

extension Unit {
    internal nonisolated(unsafe) static let wattsPerSquareMetre = Unit(symbol: "W/㎡")
}

…but then Unit isn't Sendable, so it's the same problem essentially.

But then sendability isn't actually ensured.

Am I wrong to treat @unchecked in the same vein as force-unwrapping? i.e. a crash or data corruption waiting to happen?

It's certainly very frustrating to have to declare entire types as unchecked just because the compiler doesn't like one stored property.

Yes, that’s the point. As a superclass author you cannot guarantee that all of your subclasses actually implement Sendability correctly, so if you want to lend subtypes trade on your superclass’s Sendability promises, you need to warn the client you’re vouching for them without checking. After all, someone might create an @unchecked Sendable subclass themselves, which the language must permit because it cannot statically guarantee all acceptable implementations of Sendability (like those which use locks or atomics).

usually you can avoid force unwrapping by following the right coding patterns (i have code bases with thousands of LOC without a single !), but there are a lot of situations where @unchecked is simply unavoidable. in these situations, there is no way to write the code without either using @unchecked or dropping down to even more unsafe layers.

But by that logic nothing about class inheritance is valid, since you can never guarantee arbitrary subclass's behaviours - e.g. they might override a method and muck up the implementation.

The point of Sendable in a class hierarchy would be that the compiler requires subclasses to also be Sendable. If the subclass uses @unchecked to meet that requirement, that's on the subclass - just as it is for every use of @unchecked. It shouldn't be the only option, though.

Using @unchecked shouldn't be required for code the compiler can perfectly well validate. Nor, more broadly, for perfectly valid code. Surely it should only be a transition mechanism, or at best a very rare escape hatch for esoteric situations which just aren't worth the cost of supporting properly. It shouldn't be required in every case.

It'd be like if we never had optional unwrapping syntax, and instead were told "just check != nil first, then force-unwrap".

Alas, we don’t have a LiskovSubstitutable protocol. :)

@unchecked Sendable does give you that, at least.

@unchecked only applies to Sendable conformances.

I too am having an issue with the arcane nature of Measurement. My issue arises because I do this kind of stuff:

public struct DatedMeasurement<T:Unit> {
    public let date: Date
    public let sample: Measurement<T>
    public init(date: Date, sample: Measurement<T>) {
        self.date = date
        self.sample = sample
    }
}

AND I also have concrete subclasses of Unit such as:

class UnitCount:Unit {
    class var bpm:UnitCount {
        UnitCount(symbol: "bpm")
    }
}

I have A LOT of generic structs that embed measurements somewhere along the line, so adding

Unit & Sendable

to every single one seems wrong. Remember: this is a struct.

I understand I can make UnitCount Sendable just fine, but again, the main issue is with the tens of <T:Unit> generics I have. Thoughts?