`static let` in enum vs struct?

I saw this in a colleague's PR recently, and was wondering if there was a practical difference between the following:

enum Constants {
    // no cases
    static let animationDuration: TimeInterval = 1.5
}

struct Constants {
    static let animationDuration: TimeInterval = 1.5
    private init() { }
}

The argument posed for using an enum with static lets was that the enum could never be instantiated, but I see no difference if we were to use a struct with no instance properties and a private init.

Is there a functional/language difference between these two? There is not a difference in usage at the callsite, but I was wondering if one is recommended over the other? Using an enum with no cases "feels wrong"

4 Likes

I personally use enums for namespacing. I think it’s also a little cleaner because you don’t have to maintain an empty private init.

Even Apple uses enums for namespacing, for example in the Combine framework.

5 Likes

Oh interesting, I see how they use it. Very cool, I think I'll start pushing this to my team, thanks!

Yes. A struct with a private init is still constructible: in principle, you could be given one. In your example with struct Constants, you could write the following code:

func foo(_ c: Constants) { }

and the compiler would not object.

However, an enum with no cases is literally non-constructible: no-one can build one. The technical term is that the type is “uninhabited”: the type exists, but there are no concrete values of the type. A struct with no storage is not uninhabited: there is one concrete value of the type, the value with nothing in it.

This is why the caseless enum is preferred: it more accurately communicates that there will never be a value of this type in the program.

13 Likes

There was another reason to prefer enums over structs, but I can‘t really remember it. Something about init from extension, which is only possible for structs.

1 Like

If it feels wrong, it won't. It never felt wrong to me, but it didn't feel right. And then it did and stayed that way.

Also, the name of that type is wrong. Each of the static lets is a Constant, not a Constants.

Maybe a bit unrelated, but there's also this pattern in a lot of Apple's frameworks, when they use a struct as a container for static let properties of itself. This to me has always been confusing as to why not just use enum cases... Feels like I'm missing something important here. An example would be Apple HealthKit framework and its HKCorrelationTypeIdentifier (and many many others).

struct HKCorrelationTypeIdentifier {
    static let bloodPressure: HKCorrelationTypeIdentifier
    static let food: HKCorrelationTypeIdentifier
    // etc.
}
2 Likes

I believe it's because, internally, the framework can actually create instances of the type to pass around. You can't do that with uninhabited enums.

Relatedly, structs can have private stored properties, which isn't possible for enums.

Yes but the question is why don't they use regular enum cases instead? What is the difference between the next two types, from the usage perspective:

struct HKCorrelationTypeIdentifier {
    static let bloodPressure: HKCorrelationTypeIdentifier
    static let food: HKCorrelationTypeIdentifier
    // etc.
}

and

enum HKCorrelationTypeIdentifier {
    case bloodPressure
    case food
    // etc.
}

These are imported Objective-C types, so there are various limitations in capability and design, as well as the requirement of compatibility with other Obj-C code. In this case, HKCorrelationTypeIdentifier is actually declared in Obj-C as typedef NSString *HKCorrelationTypeIdentifier; for whatever reason. In Swift this is expressed as a new RawRepresentable type with static members so as to hide the constants from Swift's global namespace when importing HealthKit.

4 Likes

The full (storage) interface of an enum must be as visible as the enum itself. Anyone who can see the enum type declaration can see all of the cases and all of their associated values.

With a struct, you could have some sort of private StorageHelper type which you don't expose outside the struct itself—this isn't possible with enums.

Mutability is also a bit of a pain with enums. You can emulate mutable stored properties by explicitly specifying a getter and setter, but it ends up being pretty verbose compared to the equivalent for a struct:

enum E {
    case c(Int)
    case d

    var val: Int? {
        get {
            switch self {
            case .c(let val):
                return val
            case .d:
                return nil
            }
        }
        set {
            if let val = newValue {
                self = .c(val)
            } else {
                self = .d
            }
        }
    }
}
5 Likes

You might want to actually use the struct for something. The constants are in some way related to the purpose of the struct. This is similar to Int.max, Double.pi and so forth.

2 Likes

A quick question about that enum. Can we provide a clear way to tell teams that what situation we should use enum itself, and when to use enum with static let to define const?

The purpose is not to write the enum with static let everywhere to confuse the usage of enum itself.