-1

I was wondering how I can enforce the following restrictions on this struct:

struct QandA {
    let questions: [String]  // Question to be displayed per page
    let mcqs: [[String]]  // Prompts to be displayed per page
    let answers: [Int]  // Answer will be the correct index
}
  • questions.count == mcqs.count == answers.count
  • forEach mcq in mcqs, mcq.count == 4 (must only be 4 options per mcq)
  • forEach answer in answers, answer < 4 (correct index must be 0...3)

The reason for this is that I think it would be very easy to perhaps, add an extra question, or even say the correct answer is 4 when I meant to put 3 (off by 1 error), and also add an extra prompt when there are only meant to be 4 multiple choices per question.

To protect myself from these errors here is what I think I can do:

Option 1

init?(questions: [String], mcqs: [[String]], answers: [Int]) {
        guard
            questions.count == mcqs.count && mcqs.count == answers.count,
            mcqs.allSatisfy({ $0.count == 4 }),
            answers.allSatisfy({ $0 < 4 })
        else {
            return nil
        }

        self.questions = questions
        self.mcqs = mcqs
        self.answers = answers
    }

Option 2

init(questions: [String], mcqs: [[String]], answers: [Int]) {
        assert(questions.count == mcqs.count && mcqs.count == answers.count)
        assert(mcqs.allSatisfy({ $0.count == 4 }))
        assert(answers.allSatisfy({ $0 < 4 }))
        
        self.questions = questions
        self.mcqs = mcqs
        self.answers = answers
    }

Option 3

Have my Views deal with these issues and have no management code for initialisation; perhaps use try-catch blocks somewhere?

Option 4

Just make sure I don't mess up when creating QandA's

My question is whether I should not allow a QandA instance to be initialised unless these restrictions are met (option 1); or should I add preconditions / asserts to not allow my code to continue if I mess up somewhere (option 2); or should my app just allow these things and me as a coder should take on the full responsibility of making sure the data is consistent, possibly making use of error handling (option 3 & 4). Perhaps using a @propertyWrapper may be the best option? What even are the pro's and con's of these different approaches?

I'm also wondering what is considered "best practice" for situations like this.

Thanks in advance.

rayaantaneja
  • 1,182
  • 7
  • 18

1 Answers1

2

If you really want to exclude invalid states, then you could consider modelling your quiz like this:

struct QandA {
    let question: String

    struct Right {
        let value: String
    }
    struct Wrong {
        let value: String
    }

    enum Options {
        case A(Right, Wrong, Wrong, Wrong)
        case B(Wrong, Right, Wrong, Wrong)
        case C(Wrong, Wrong, Right, Wrong)
        case D(Wrong, Wrong, Wrong, Right)
    }

    let options: Options
}

struct Quiz {
    let questions: [QandA]
}

Here you can't have more than 4 (3 wrong + 1 right), and you must have exactly one right answer in the options.

If you find that you have two collections, e.g. [A] and [B] and you need them to be the same size always, you probably mean to say you have an array of pairs of A's and B's, like [(A, B)], and often where you have tuples, like pairs, then there's a named Struct that's waiting to be created.

You almost always want to make your data structure smart so your algorithms don't have to be. Use knowledge of "type algebra" to refactor invalid states out of existence - a classic example you can find online if you search, is the refactoring of the types in the completion handler of URLSession dataTask(with:completionHandler:) - some of which are not valid - into a type that make those invalid states inexpressible. The key idea is that sum types (enum) and product types (structs) can be thought of just like algebra A+B and A*B, and can be literally refactored just the same.

Shadowrun
  • 3,572
  • 1
  • 15
  • 13