Can I define an enumeration as a subset of other enumeration cases?

Note: This is basically the same question as the other I posted yesterday on Stackoverflow. However, I thought I was using a bad example in this matter, which did not quite shift it to the point of what I had in mind. Since all the answers to this original post relate to this first question, I thought it would be better to introduce a new example in a separate question - no duplication is needed.


Model characters that can move

Define an enumeration of directions for use in a simple game:

enum Direction { case up case down case left case right } 

Now in the game I need two types of characters:

  • A HorizontalMover that can only move left and right.
    ← β†’
  • A VerticalMover that can only move up and down.
    ↑ ↓

They can move, so they both implement

 protocol Movable { func move(direction: Direction) } 

So, let's define two structures:

 struct HorizontalMover: Movable { func move(direction: Direction) let allowedDirections: [Direction] = [.left, .right] } struct VerticalMover: Movable { func move(direction: Direction) let allowedDirections: [Direction] = [.up, .down] } 

Problem

... with this approach is that I can pass invalid values to the move() function, for example. the following call will be made:

 let horizontalMover = HorizontalMover() horizontalMover.move(up) // ⚑️ 

Of course, I can check inside the move() funtion whether the passed direction allowed for this Mover type and otherwise throw an error. But since I have information that is allowed at compile time , I also want the check to be performed at compile time .

So I really want this:

 struct HorizontalMover: Movable { func move(direction: HorizontalDirection) } struct VerticalMover: Movable { func move(direction: VerticalDirection) } 

where HorizontalDirection and VerticalDirection are subsets of the direction enumeration.

It makes no sense to simply define two types of directions independently of each other, without any common β€œancestor”:

 enum HorizontalDirection { case left case right } enum VerticalDirection { case up case down } 

because then I will have to redefine the same cases over and over, which are semantically the same for each listing representing directions. For example. if I add another character that can move in any direction, I will also have to implement a common direction jumper (as shown above). Then I would have a case of left in the HorizontalDirection enum enumeration and a left in the general enumeration of direction , which does not know about each other, which is not only ugly, but also becomes a real problem when assigning and using the original values ​​that I would have to reassign in each listing.


So is there a way out of this?

Can I define an enumeration as a subset of cases of another enumeration like this?

 enum HorizontalDirection: Direction { allowedCases: .left .right } 
+8
enums swift subset restriction
source share
3 answers

Not. This is currently not possible with Swift enums.

Solutions I can think of:

  • Use protocols, as I outlined in your other question.
  • Refusal to check runtime
+2
source share

Here's a possible solution for compiling:

 enum Direction: ExpressibleByStringLiteral { case unknown case left case right case up case down public init(stringLiteral value: String) { switch value { case "left": self = .left case "right": self = .right case "up": self = .up case "down": self = .down default: self = .unknown } } public init(extendedGraphemeClusterLiteral value: String) { self.init(stringLiteral: value) } public init(unicodeScalarLiteral value: String) { self.init(stringLiteral: value) } } enum HorizontalDirection: Direction { case left = "left" case right = "right" } enum VerticalDirection: Direction { case up = "up" case down = "down" } 

Now we can define the move method as follows:

 func move(_ allowedDirection: HorizontalDirection) { let direction = allowedDirection.rawValue print(direction) } 

The disadvantage of this approach is that you need to make sure that the lines in your individual enums are correct, which is potentially error prone. I specifically used ExpressibleByStringLiteral for this reason, not ExpressibleByIntegerLiteral because, in my opinion, it is more readable and supported, you can disagree.

You also need to define all 3 of these initializers, which is probably a bit cumbersome, but you would not do this if you were using ExpressibleByIntegerLiteral instead.

I know that you trade security at compile time in one place for another, but I believe that such a solution may be preferable in some situations.

To make sure you don't have any foggy lines, you can also add a simple unit test, for example:

 XCTAssertEqual(Direction.left, HorizontalDirection.left.rawValue) XCTAssertEqual(Direction.right, HorizontalDirection.right.rawValue) XCTAssertEqual(Direction.up, VerticalDirection.up.rawValue) XCTAssertEqual(Direction.down, VerticalDirection.down.rawValue) 
+1
source share

You may have solved your problem, but for everyone looking for an answer, for some time (not sure when Apple introduced it), you can use the related values ​​inside the enumerations to model such states.

 enum VerticalDirection { case up case down } enum HorizontalDirection { case left case right } enum Direction { case vertical(direction: VerticalDirection) case horizontal(direction: HorizontalDirection) } 

So you can use a method like this:

 func move(_ direction: Direction) { print(direction) } move(.horizontal(.left)) 

And if you comply with the Equatable protocol:

 extension Direction: Equatable { static func ==(lhs: Direction, rhs: Direction) -> Bool { switch (lhs, rhs) { case (.vertical(let lVertical), .vertical(let rVertical)): switch (lVertical, rVertical) { case (.up, .up): return true case (.down, .down): return true default: return false } case (.horizontal(let lHorizontal), .horizontal(let rHorizontal)): switch (lHorizontal, rHorizontal) { case (.left, .left): return true case (.right, .right): return true default: return false } default: return false } } } 

you can do something like this:

 func isMovingLeft(direction: Direction) -> Bool { return direction == .horizontal(.left) } let characterDirection: Direction = .horizontal(.left) isMovingLeft(direction: characterDirection) // true isMovingLeft(direction: characterDirection) // false 
0
source share

All Articles