Note: This is basically the same question as another one I've posted on Stackoverflow yesterday. However, I figured that I used a poor example in that question that didn't quite boil it down to the essence of what I had in mind. As all replies to that original post refer to that first question I thought it might be a better idea to put the new example in a separate question — no duplication intended.
Model Game Characters That Can Move
Let's define an enum 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 kinds of characters:
- A
HorizontalMover
that can only move left and right.
← → - A
VerticalMover
that can only move up and down.
↑ ↓
They can both move so they both implement the
protocol Movable {
func move(direction: Direction)
}
So let's define the two structs:
struct HorizontalMover: Movable {
func move(direction: Direction)
let allowedDirections: [Direction] = [.left, .right]
}
struct VerticalMover: Movable {
func move(direction: Direction)
let allowedDirections: [Direction] = [.up, .down]
}
The Problem
... with this approach is that I can still pass disallowed values to the move()
function, e.g. the following call would be valid:
let horizontalMover = HorizontalMover()
horizontalMover.move(up) // ⚡️
Of course I can check inside the move()
funtion whether the passed direction
is allowed for this Mover type and throw an error otherwise. But as I do have the information which cases are allowed at compile time I also want the check to happen at compile time.
So what I really want is this:
struct HorizontalMover: Movable {
func move(direction: HorizontalDirection)
}
struct VerticalMover: Movable {
func move(direction: VerticalDirection)
}
where HorizontalDirection
and VerticalDirection
are subset-enums of the Direction
enum.
It doesn't make much sense to just define the two direction types independently like this, without any common "ancestor":
enum HorizontalDirection {
case left
case right
}
enum VerticalDirection {
case up
case down
}
because then I'd have to redefine the same cases over and over again which are semantically the same for each enum that represents directions. E.g. if I add another character that can move in any direction, I'd have to implement the general direction enum as well (as shown above). Then I'd have a left
case in the HorizontalDirection
enum and a left
case in the general Direction
enum that don't know about each other which is not only ugly but becomes a real problem when assigning and making use of raw values that I would have to reassign in each enumeration.
So is there a way out of this?
Can I define an enum as a subset of the cases of another enum like this?
enum HorizontalDirection: Direction {
allowedCases:
.left
.right
}
No. This is currently not possible with Swift enums.
The solutions I can think of:
Here's a possible compile-time solution:
Now we can define a
move
method like this:The drawback of this approach is that you need to make sure that the strings in your individual enums are correct, which is potentially error-prone. I have intentionally used
ExpressibleByStringLiteral
for this reason, rather thanExpressibleByIntegerLiteral
because it is more readable and maintainable in my opinion - you may disagree.You also need to define all 3 of those initializers, which is perhaps a bit unwieldy, but you would avoid that if you used
ExpressibleByIntegerLiteral
instead.I'm aware that you're trading compile-time safety in one place for another, but I suppose this kind of solution might be preferable in some situations.
To make sure that you don't have any mistyped strings, you could also add a simple unit test, like this: