I've read the documentation, and seen perhaps only a part of what the protocol is. I just am not following the logic. Can someone help me understand this?
What I see in xcode when I examine the protocol
/// Conforming to the CaseIterable Protocol
/// =======================================
///
/// The compiler can automatically provide an implementation of the
/// `CaseIterable` requirements for any enumeration without associated values
/// or `@available` attributes on its cases. The synthesized `allCases`
/// collection provides the cases in order of their declaration.
///
/// You can take advantage of this compiler support when defining your own
/// custom enumeration by declaring conformance to `CaseIterable` in the
/// enumeration's original declaration. The `CompassDirection` example above
/// demonstrates this automatic implementation.
public protocol CaseIterable {
/// A type that can represent a collection of all values of this type.
associatedtype AllCases : Collection = [Self] where Self == Self.AllCases.Element
/// A collection of all values of this type.
static var allCases: Self.AllCases { get }
}
I'm struggling to follow what is happening here and why. Can someone walk me through the logic of this please?
One of the other big struggles I'm having because of this is if I conform a protocol to be CaseIterable.
protocol Foo: CaseIterable {}
I can't use it as a variable anymore.
struct Bar {
var foo: Foo
}
I get this error
Protocol 'Foo' can only be used as a generic constraint because it has Self or associated type requirements
.
It does have Self requirements but I can't figure out how to get around this problem. If someone could help me understand why this happens and how to fix it too, I'd be very grateful.
Edit: - This is the playground code copied directly. I've updated it to use the some, but I'm not quite sure how to proceed past this error.
import Foundation
protocol Zot: CaseIterable {
var prop: Data { get }
}
enum Bar: Zot {
case thing3
case thing4
var prop: Data {
switch self {
case .thing3, .thing4: return Data()
}
}
init() {}
}
enum Baz: Zot {
case thing1
case thing2
var prop: Data {
switch self {
case .thing1, .thing2: return Data()
}
}
init() {}
}
enum Foo {
case bar
case baz
var otherValues: some Zot {
switch self {
case .bar:
return Bar
case .baz:
return Baz
}
}
}
CaseIterable
exists to allow you to programmatically walk through all the possible cases of an enum
, allowing you use the enum
type as a Collection
of its cases:
enum CardinalDirection: CaseIterable { case north, south, east, west }
for direction in CardinalDirection.allCases {
// Do something with direction which is one of north, south, east, west
print("\(direction)")
}
This prints
north
south
east
west
There is nothing that prevents you from making other kinds of types conform to CaseIterable
; however, the compiler will only synthesize conformance for enum
types. It's not useful for most other kinds of types; however, I have occasionally found it useful for types that conform to OptionSet
. In that case you have to manually implement conformance.
struct AssetFlags: OptionSet, CaseIterable
{
typealias RawValue = UInt8
typealias AllCases = [AssetFlags]
let rawValue: RawValue
static let shouldPreload = AssetFlags(rawValue: 0x01)
static let isPurgeable = AssetFlags(rawValue: 0x02)
static let isLocked = AssetFlags(rawValue: 0x04)
static let isCached = AssetFlags(rawValue: 0x08)
static var allCases: AllCases = [shouldPreload, isPurgeable, isLocked, isCached]
}
Note that OptionSet
is conceptually similar to an enum
. They both define a small set of distinct values they can have. With one they are mutually exclusive, while for the other they may be combined. But the key thing for CaseIterable
to be useful is the finite nature of the set of possible distinct values. If your type has that characteristic, conforming to CaseIterable
could be useful, otherwise, it wouldn't make sense. Int
or String
, for example, are not good candidates for CaseIterable
.
In my own code, I don't bother conforming to CaseIterable
, even for enum
types, until a specific need arises that requires it. Actually I take that approach to all protocol conformance. It's a specific case of the more general YAGNI rule of thumb: "You ain't gonna need it."
Regarding your Bar
struct, the problem is not specifically related to CaseIterable
, but rather to using a protocol with Self or associated type requirements, which for Foo
happens to be inherited from CaseIterable
.
Swift 5.7 relaxed the rules concerning Self and associated type requirements a bit, allowing you to use the any
keyword to tell the compiler you want to use an existential Foo
instead of a concrete Foo
to write
struct Bar {
var foo: any Foo
}
If you want a concrete Foo
you could use some
. The original way to do it though, which still works, is to make Bar
explicitly generic
struct Bar<T: Foo> {
var foo: T
}
The way you're using enums
is... well, let's say it's out of the ordinary. There are two problems. The first is that you're returning types not values:
enum Foo {
case bar
case baz
// Will return a *value* of a type that conforms to Zot
var otherValues: some Zot
{
switch self {
case .bar:
return Bar // Bar is a *type* not a value
case .baz:
return Baz // Baz is a *type* not a value
}
}
}
I'll fix this is in a way that is almost certainly wrong for what you want to do, but allows moving forward to the other problem. We need to return values, so I'll just pick the first of the corresponding cases of Bar
and Baz
, and that will expose the other problem.
enum Foo {
case bar
case baz
var otherValues: some Zot
{
switch self {
case .bar: return Bar.thing3
case .baz: return Baz.thing1
}
}
}
The problem here is that some
means that there will be one specific concrete type that conforms to Zot
, so the compiler will be able to access its properties and methods directly rather than via its protocol witness table... it's basically a way to have the efficiency of having the calling code use the concrete types without having to tie to calling code to the concrete type at the source code level. otherValues
, however, returns a value of either of two types, so the return type would have to be an existential type rather than a concrete one. You could do this if you return any Zot
instead of some Zot
.
Of course even using any
, this version is wrong, because it doesn't take into account half of the cases of Bar
and Baz
. I assume that you want to be able to construct a Foo
from a Bar
or Baz
while preserving its original value somehow, and I guess retrieve it later.
Before I present solutions, I want to mention that without knowing exactly what you are trying to accomplish, your code feels like it took a wrong design turn at some point. It would probably be better to rethink how you're doing what you want to do to see if there is a better way.
If I understand what your trying to do, I can think of at least three of ways, none of which requires CaseIterable
, but maybe that's needed for other reasons.
The first is to define Foo
so that it explicitly contains all of the cases of Bar
and Baz
:
enum Foo: Zot
{
case thing1, thing2, thing3, thing4
init(_ value: Bar)
{
switch value
{
case .thing3: self = .thing3
case .thing4: self = .thing4
}
}
init(_ value: Baz)
{
switch value
{
case .thing1: self = .thing1
case .thing2: self = .thing2
}
}
var prop: Data
{
switch self
{
case .thing1: return Baz.thing1.prop
case .thing2: return Baz.thing2.prop
case .thing3: return Bar.thing3.prop
case .thing4: return Bar.thing4.prop
}
}
var bazValue: Baz?
{
switch self
{
case .thing1: return .thing1
case .thing2: return .thing2
default: return nil
}
}
var barValue: Bar?
{
switch self
{
case .thing3: return .thing3
case .thing4: return .thing4
default: return nil
}
}
}
This has the advantage of being straight-forward, but will require more maintenance if you add/remove cases from Bar
or Baz
- or even add a whole other enum
that conforms to Zot
.
The second way is to define Foo
so that it uses associated values:
enum Foo
{
case bar(value: Bar)
case baz(value: Baz)
init(_ value: Bar) { self = .bar(value: value) }
init(_ value: Baz) { self = .baz(value: value) }
}
I think this second case is cleaner, and you don't need otherValues
because usage code can do:
switch foo
{
case let .bar(value: bar):
// do whatever with bar
case let .baz(value: baz):
// do whatever with baz
}
Still assuming second version of Foo
, maybe an even cleaner way is:
extension Foo
{
func withZotValue<R>(_ code: (any Zot) throws -> R) rethrows -> R
{
switch self
{
case let .bar(value: value): return try code(value)
case let .baz(value: value): return try code(value)
}
}
}
That allows you to eliminate a lot of switch
statements in usage code. To use it:
foo.withZotValue { zotValue in
// Do something with zotValue that the Zot protocol supports.
}
If you need this second version of Foo
to conform to CaseIterable
, Swift won't synthesize conformance for you because of the associated values, but you can write the conformance yourself.
extension Foo: CaseIterable
{
typealias AllCases = [Self]
static var allCases: AllCases {
[
Baz.allCases.map { .baz(value: $0) },
Bar.allCases.map { .bar(value: $0) },
].joined()
}
}
The last possible solution, which actually should probably be the first, if it applies, would be to define whatever you're trying to do that's common to both Bar
and Baz
in Zot
. Whether that's a good idea or not depends on what you're trying to do, but let's assume it does make sense. For example, I notice that both Bar
and Baz
support a prop
property, but the Zot
protocol doesn't list prop
. Why not? Is it unrelated to "Zotness"? If anything that conforms to Zot
should have a prop
property, then add it to the protocol:
protocol Zot: CaseIterable {
var prop: Data { get }
}
You still provide implementations of prop
in Bar
and Baz
- that's kind of like overriding a base class method in subclasses.
Let's say the Data
returned from prop
is encoded JSON and you want be able to decode a Codable
thing from a Zot
. You can now do that without caring if its a Bar
or a Baz
(or any other new Zot
-conforming type you might add later):
func decode<T: Codable>(_ type: T.Type, from src: some Zot) throws -> T {
return try JSONDecoder().decode(T.self, from: src.prop)
}