11

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

Community
  • 1
  • 1
Mischa
  • 15,816
  • 8
  • 59
  • 117
  • In Java, this would be more difficult to achieve at compile time and much easier to do at run time. Will your employees be writing code themselves? If not, then there is no mandate to provide rigid type safety to your subtypes. It would be simpler to do the error checking on sets in code at run time. – scottb Nov 16 '16 at 16:37
  • 1
    It seems you are trying to put your constraints to the enum instead of keeping them where they belong - to the type that uses the enum. That doesn't look like a scalable architecture. – Sulthan Nov 16 '16 at 17:02
  • You could probably do that by making every `case` a struct and then adding protocol `AlmondCakeIngredient` to every ingredient that can be used to make an almond cake but you will run into other problems with scalability. – Sulthan Nov 16 '16 at 17:07
  • Also, adding a new ingredient will require you to recompile code. Why don't you keep ingredients as data in an external file, for example? – Sulthan Nov 16 '16 at 17:12
  • @scottb: My "employees" were only a metaphor to remain in the sample domain of the bakery. I reality I will be writing the code or some of my teammates. It was only a matter of saying "I want type saftey (at compile time)". ;) And you're right: It seems like it's *a lot* easier to do the check at run time not only in Java, but in Swift as well. – Mischa Nov 16 '16 at 22:49
  • @Sulthan: Are you sure about your first statement? I mean, if you define a random function, say `func f(x: Float)`, you're restricting x to be a `Float`, it can't be of `Any` type. Equally, if you allow all ingredients for baking a cake and use `func bake(with ingredients: [Ingredient]) -> Cake` you're restricting the input parameter to only contain `Ingredient`s. Thus, in my opinion restricting the values allowed as a parameter of a function is pretty common. So why not defining another type that only contains a subset of the cases of an existing enum and restrict parameters to that type? – Mischa Nov 16 '16 at 23:09
  • @Sulthan: Your suggestion to use `struct`s instead of cases has been outlined by @mathiasnagler as well and I have left my thoughts on that approach in two comments to his answer. – Mischa Nov 16 '16 at 23:12

5 Answers5

2

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.

sdasdadas
  • 23,917
  • 20
  • 63
  • 148
  • 2
    This would probably be better implemented as a `Set`, for constant time lookup. – Hamish Nov 16 '16 at 17:26
  • That's of course a solution that would work which is implemented nicely by @ganzogo in his answer. And I agree that using a `Set` instead of an `Array` even improves performance for a large number of `Ingredient`s. However (and I forgot to mention that explicitly in my question), this approach doesn't doesn't give me compile time safety which is what I thought of when writing *"I still want to make sure they only use the right ingredients to bake a cake"*. And as a matter of fact, the allowed `Ingredient`s (cases) are already known at compile time. – Mischa Nov 16 '16 at 21:59
1

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.

ganzogo
  • 2,516
  • 24
  • 36
  • This is a really neat implementation and I'm likely to end up using a similar one if there is absolutely no way to perform the check at a compile time using `enum`s. Thanks! (I was actually searching for a compile time solution but didn't say it explicitly.) I would suggest one little improvement for performance: If you enumerate over the ingredients instead of using the `reduce` function you can break from the type check as soon as the first disallowed ingredient is found. – Mischa Nov 16 '16 at 22:18
  • 1
    That's true. And as has been mentioned elsewhere, the valid ingredients might be better as a Set. – ganzogo Nov 16 '16 at 22:22
  • Hi @ganzogo, from your solution above, how can the `Ingredient` enum be available only to those adopting the `Cake` protocol? Currently, other files have access to `Ingredient` enum. – yohannes Sep 04 '18 at 07:04
1

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.

naglerrr
  • 2,809
  • 1
  • 12
  • 24
  • 1
    Also, the enum won't really behave like an enum anymore. You will also need a protocol `Ingredient` implemented by every ingredient and an identifier to compare them. – Sulthan Nov 16 '16 at 17:10
  • That is certainly true. As I said at the end of my post I think that you shouldn't probably do this despite it solving the question. All of these implementations do not guarantee that a caller to `bake(with:)` passes in exactly the correct parameters at compile time - making it kind of pointless. – naglerrr Nov 16 '16 at 17:13
  • I chose my example poorly. I made a little abstraction from my actual problem with the intention of making the example easier to grasp but I did miss this (pretty obvious) loophole. In the actual problem I'm facing I'm only passing one parameter of that `enum` type, not an array. In that case there would be no such loophole — I would always have to pass one particular enum case. – Mischa Nov 16 '16 at 22:31
  • I agree with @Sulthan: The problem of this implementation is that I don't get the normal `enum` behavoir that would totally make sense here. Furthermore, it feels a little like misusing the Swift constructs for purposes they are not intended for. At least, the code feels a little unintuitive to me. It's not expressive in way that it doesn't show that a `ChocolateCakeIngredient` *is* actually an `Ingredient`. – Mischa Nov 16 '16 at 22:44
  • That is because it doesn't need to. In the code above it is totally possible to implement `ChocolateCakeIngredient` in a type that is not an ingredient. The methods type constraint is "something that implements `ChocolateCakeIngredient`". – naglerrr Nov 17 '16 at 13:10
1

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 ...
dfrib
  • 70,367
  • 12
  • 127
  • 192
-1

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.

Ahmad F
  • 30,560
  • 17
  • 97
  • 143
  • Thank you for your answer. This code will probably work for the particular case outlined in my question but it doesn't make much sense from an architectural perspective in my opinion: You're baking a cake, not an ingredient, so why would you call `bake(aReceipe)` on a particular ingredient? Furthermore, if I add ingredients later on I'd have to overhaul every single cake and check for the new ingredients → no scalable. – Mischa Nov 16 '16 at 23:21
  • @Mischa You're right... somehow, I can feel a bad a "code smell". Thankfully, I didn't got any downvotes yet :) I will keep it, it might gives an idea... Do you think that should I delete it? – Ahmad F Nov 17 '16 at 04:52