Can I limit the listing to some cases of another listing?

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 } 

The rawValue case represents the inventory number.

Then I have two recipes:

Chocolate cake:

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

Almond cake:

  • 300 g of flower
  • 200 g sugar
  • 20 g yeast
  • 200 g 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 that they use only the right ingredients to bake the cake. 😉

I could do this by specifying two separate enumerations, for example:

 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 the cake as follows:

 // 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 will 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 this - especially since the enumerations include inventory numbers such as rawValue s.

This leads me to the question of is there a way in Swift to restrict an enumeration to some cases of another enumeration ? 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 such an essay possible? How can i do this?

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


Update

Of all the comments and answers to this question, I thought that the example that I chose for this question was a little inappropriate, because it did not reduce the essence of the problem and left a loophole regarding type safety.

Since all of the posts on this page relate to this particular example, I created a new question in Stackoverflow with an example that is easier to understand and hit a nail on my head:

➡️ Same question with a more specific example

+7
enums swift restriction
source share
5 answers

I think you should list the ingredients for specific recipes, like:

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

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

+2
source share

I do not think that such a check can be performed at compile time. Here is one way to structure your code for 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 is as follows:

 func bake<C: Cake>(ingredients: [Ingredient]) -> C { guard C.areIngredientsAllowed(ingredients) else { fatalError() } let cake = C() // TODO: Let bake! return cake } 

Now I can say:

 let almondCake: AlmondCake = bake(ingredients: ingredients) 

... and make sure that only valid ingredients are used.

+1
source share

You can 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 use enum Ingredients as a namespace for all of my interlocutors. It also helps with code completion.

Then create a protocol for each Recipe and combine the ingredients that go into this recipe for this protocol.

Although this should solve your question, I'm not sure you should do this. This (as well as your pseudo-code) will ensure that no one can pass on an ingredient that does not belong to chocolate cake when baking. However, it will not prevent anyone from trying to call bake(with ingredients:) with an empty array or something similar. Because of this, you will not actually get any security in your design.

+1
source share

Alternative approach: using option set type

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

Another approach is to provide the Ingredient type of OptionSet (a type that conforms to the OptionsSet protocol):

eg.

 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] } 

For your example, bake(with:) , where the / dev employee is trying to implement baking 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 calling for bake(with:) using data from the 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 ... 
+1
source share

Static solution:

If the number of recipes is always the same, you can use the function in the enumeration:

  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 */ } } } 

Using:

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

Dynamic solution:

If the number of recipes is variable, and this is what I assume, I cheated a little using a separate model class :)

It will be as follows:

 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: } } } 

Using:

 // 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 this is not the best solution for your case, I hope this helps.

0
source share

All Articles