Home » Swift » Can I restrict an enum to certain cases of another enum?

Can I restrict an enum to certain cases of another enum?

Posted by: admin January 4, 2018 Leave a comment

Questions:

Say I have a bakery and an inventory of ingredients:

enum Ingredient {
    case flower     = 1
    case sugar      = 2
    case yeast      = 3
    case eggs       = 4
    case milk       = 5
    case almonds    = 6
    case chocolate  = 7
    case salt       = 8
}

A case’s rawValue represents the inventory number.

Then I have two recipes:

Chocolate Cake:

  • 500g flower
  • 300g sugar
  • 3 eggs
  • 200ml milk
  • 200g chocolate

Almond Cake:

  • 300g flower
  • 200g sugar
  • 20g yeast
  • 200g almonds
  • 5 eggs
  • 2g salt

Now I define a function

func bake(with ingredients: [Ingredient]) -> Cake

Of course I trust my employees, but I still want to make sure they only use the right ingredients to bake a cake. 😉

I could do this by defining two separate enums like this:

enum ChocolateCakeIngredient {
    case flower
    case sugar
    case eggs
    case milk
    case chocolate
}

enum AlmondCakeIngredient {
    case flower
    case sugar
    case yeast
    case eggs
    case almonds
    case salt
}

and bake a cake like this:

// in chocolate cake class / struct:
func bake(with ingredients: [ChocolateCakeIngredient]) -> ChocolateCake
// in almond cake class / struct:
func bake(with ingredients: [AlmondCakeIngredient]) -> AlmondCake

But then I would have to redefine the same ingredients over and over again as many ingredients are used for both cakes. I really don’t want to do that – especially as there are inventory numbers attached to the enum cases as rawValues.

That leads me to the question if there is a way in Swift to restrict an enum to certain cases of another enum? Something like (pseudo code):

enum ChocolateCakeIngredient: Ingredient {
    allowedCases:
        case flower
        case sugar
        case eggs
        case milk
        case chocolate
}

enum AlmondCakeIngredient: Ingredient {
    allowedCases:
        case flower
        case sugar
        case yeast
        case eggs
        case almonds
        case salt
}

Is a composition like this possible? How can I do it?

Or maybe there is another pattern I can use for this scenario?


Update

From all the comments and answers to this question I figured that the example I picked for this question was a little inappropriate as it didn’t boil down the essence of the problem and left a loophole regarding type safety.

As all posts on this page relate to this particular example, I created a new question on Stackoverflow with an example that’s easier to understand and hits the nail on its head:

➡️ Same question with a more specific example

Answers:

I think you should list ingredients for specific recipes as:

let chocolateCakeIngredients: [Ingredient] = [.flower, ...]

and then just check if that list contains the required ingredient.

Questions:
Answers:

You could do something like this in Swift:

enum Ingredients {
    struct Flower { }
    struct Sugar { }
    struct Yeast { }
    struct Eggs { }
    struct Milc { }
}

protocol ChocolateCakeIngredient { }
extension Sugar: ChocolateCakeIngredient { }
extension Eggs: ChocolateCakeIngredient { }
...

func bake(ingredients: [ChocolateCakeIngredient]) { }

In this example i am using the enum Ingredients as a namespace for all my ingedients. This also helps with code completion.

Then, create a protocol for each Recipe and conform the ingredients that go in that recipe to that protocol.

While this should solve your question, I am not sure that you should do this. This (and also your pseudo-code) will enforce that no one can pass a ingredient that does not belong into a chocolate cake when baking one. It will, however, not prohibit anyone to try and call bake(with ingredients:) with an empty array or something similar. Because of that, you will not actually gain any safety by your design.

Questions:
Answers:

An Alternative Approach: Using an Option Set Type

Or maybe there is another pattern I can use for this scenario?

Another approach is letting your Ingredient be an OptionSet type (a type conforming to the protocol OptionsSet):

E.g.

struct Ingredients: OptionSet {
    let rawValue: UInt8

    static let flower    = Ingredients(rawValue: 1 << 0) //0b00000001
    static let sugar     = Ingredients(rawValue: 1 << 1) //0b00000010
    static let yeast     = Ingredients(rawValue: 1 << 2) //0b00000100
    static let eggs      = Ingredients(rawValue: 1 << 3) //0b00001000
    static let milk      = Ingredients(rawValue: 1 << 4) //0b00010000
    static let almonds   = Ingredients(rawValue: 1 << 5) //0b00100000
    static let chocolate = Ingredients(rawValue: 1 << 6) //0b01000000
    static let salt      = Ingredients(rawValue: 1 << 7) //0b10000000

    // some given ingredient sets
    static let chocolateCakeIngredients: Ingredients = 
        [.flower, .sugar, .eggs, .milk, .chocolate]
    static let almondCakeIngredients: Ingredients = 
        [.flower, .sugar, .yeast, .eggs, .almonds, .salt]
}

Applied to your bake(with:) example, where the employee/dev attempts to implement the baking of a chocolate cake in the body of bake(with:):

/* dummy cake */
struct Cake {
    var ingredients: Ingredients
    init(_ ingredients: Ingredients) { self.ingredients = ingredients }
}

func bake(with ingredients: Ingredients) -> Cake? {
    // lets (attempt to) bake a chokolate cake
    let chocolateCakeWithIngredients: Ingredients = 
        [.flower, .sugar, .yeast, .milk, .chocolate]
                        // ^^^^^ ups, employee misplaced .eggs for .yeast!

    /* alternatively, add ingredients one at a time / subset at a time
    var chocolateCakeWithIngredients: Ingredients = []
    chocolateCakeWithIngredients.formUnion(.yeast) // ups, employee misplaced .eggs for .yeast!
    chocolateCakeWithIngredients.formUnion([.flower, .sugar, .milk, .chocolate]) */

    /* runtime check that ingredients are valid */
    /* ---------------------------------------- */

    // one alternative, invalidate the cake baking by nil return if 
    // invalid ingredients are used
    guard ingredients.contains(chocolateCakeWithIngredients) else { return nil }
    return Cake(chocolateCakeWithIngredients)

    /* ... or remove invalid ingredients prior to baking the cake 
    return Cake(chocolateCakeWithIngredients.intersection(ingredients)) */

    /* ... or, make bake(with:) a throwing function, which throws and error
       case containing the set of invalid ingredients for some given attempted baking */
}

Along with a call to bake(with:) using the given available chocolate cake ingredients:

if let cake = bake(with: Ingredients.chocolateCakeIngredients) {
    print("We baked a chocolate cake!")
}
else {
    print("Invalid ingredients used for the chocolate cake ...")
} // Invalid ingredients used for the chocolate cake ...

Questions:
Answers:

I don’t believe it is possible to perform a check like this at compile time. Here is one way to structure your code to do this at runtime:

enum Ingredient: Int {
  case flour = 1
  case sugar = 2
  case yeast = 3
  case eggs = 4
  case milk = 5
  case almonds = 6
  case chocolate = 7
  case salt = 8
}

protocol Cake {
  init()
  static var validIngredients: [Ingredient] { get }
}

extension Cake {
  static func areIngredientsAllowed(_ ingredients: [Ingredient]) -> Bool {
    for ingredient in ingredients {
      if !validIngredients.contains(ingredient) {
        return false
      }
    }
    return true
  }
}

class ChocolateCake: Cake {
  required init() {}
  static var validIngredients: [Ingredient] = [.flour, .sugar, .eggs, .milk, .chocolate]
}

class AlmondCake: Cake {
  required init() {}
  static var validIngredients: [Ingredient] = [.flour, .sugar, .yeast, .eggs, .almonds, .salt]
}

The bake method looks like this:

func bake<C: Cake>(ingredients: [Ingredient]) -> C {

  guard C.areIngredientsAllowed(ingredients) else {
    fatalError()
  }

  let cake = C()
  // TODO: Let's bake!
  return cake
}

Now I can say:

let almondCake: AlmondCake = bake(ingredients: ingredients)

… and be sure that only valid ingredients were used.

Questions:
Answers:

Static Solution:

If the recipe quantities are always the same, you can use a function in the enum:

    enum Ingredient {
        case chocolate
        case almond

        func bake() -> Cake {
            switch self {
            case chocolate:
                print("chocolate")
                /*
                 return a Chocolate Cake based on:

                 500g flower
                 300g sugar
                 3 eggs
                 200ml milk
                 200g chocolate
                 */
            case almond:
                print("almond")
                /*
                 return an Almond Cake based on:

                 300g flower
                 200g sugar
                 20g yeast
                 200g almonds
                 5 eggs
                 2g salt
                 */
            }
        }
    }

Usage:

// bake chocolate cake
let bakedChocolateCake = Ingredient.chocolate.bake()

// bake a almond cake
let bakedAlmondCake = Ingredient.almond.bake()

Dynamic Solution:

If the recipe quantities are changeable -and that’s what I assume-, I cheated a little bit by using a separated model class 🙂

It will be as the following:

class Recipe {
    private var flower = 0
    private var sugar = 0
    private var yeast = 0
    private var eggs = 0
    private var milk = 0
    private var almonds = 0
    private var chocolate = 0
    private var salt = 0

    // init for creating a chocolate cake:
    init(flower: Int, sugar: Int, eggs: Int, milk: Int, chocolate: Int) {
        self.flower = flower
        self.sugar = sugar
        self.eggs = eggs
        self.milk = milk
        self.chocolate = chocolate
    }

    // init for creating an almond cake:
    init(flower: Int, sugar: Int, yeast: Int, almonds: Int, eggs: Int, salt: Int) {
        self.flower = flower
        self.sugar = sugar
        self.yeast = yeast
        self.almonds = almonds
        self.eggs = eggs
        self.salt = salt
    }
}

enum Ingredient {
    case chocolate
    case almond

    func bake(recipe: Recipe) -> Cake? {
        switch self {
        case chocolate:
            print("chocolate")
            if recipe.yeast > 0 || recipe.almonds > 0 || recipe.salt > 0 {
                return nil
                // or maybe a fatal error!!
            }

            // return a Chocolate Cake based on the given recipe:
        case almond:
            print("almond")
            if recipe.chocolate > 0 {
                return nil
                // or maybe a fatal error!!
            }

            // return an Almond Cake based on the given recipe:
        }
    }
}

Usage:

// bake chocolate cake with a custom recipe
let bakedChocolateCake = Ingredient.chocolate.bake(Recipe(flower: 500, sugar: 300, eggs: 3, milk: 200, chocolate: 200)

// bake almond cake with a custom recipe
let bakedAlmondCake = Ingredient.chocolate.bake(Recipe(flower: 300, sugar: 200, yeast: 20, almonds: 200, eggs: 5, salt: 2))

Even if those are not the optimal solution for your case, I hope it helped.