Overview
Not every student has been graded yet, and Swift has a specific, type-checked way to represent "this value might genuinely not exist": the Optional. A `Student` whose `marks` field is typed `Int?` instead of plain `Int` is not a workaround — it is the compiler forcing every piece of code that reads `marks` to explicitly decide what "no marks yet" means for that particular operation, rather than smuggling a sentinel value like `-1` through the program and hoping every reader remembers what it means.
By the end of this tutorial you will have a console application that stores a roster of `Student` values with optional marks, an `enum Grade` that turns a numeric score into a closed set of letter grades, and a report generator that safely unwraps every optional along the way — using `guard let` to handle the "ungraded" case early, and `compactMap` to compute a class average over only the students who actually have a score.
- A `Student` struct with a genuinely optional `marks: Int?` field.
- A `Grade` enum with raw `String` values mapping scores to letter grades.
- A `gradeForMarks(_:)` function using a `switch` over numeric ranges.
- A `summary(for:)` function that uses `guard let` to handle ungraded students.
- A `classAverage(_:)` function using `compactMap` and `reduce` to average only recorded scores.
- A `readLine()`-driven menu loop for adding students and generating the report.
Prerequisites
- Structs — stored properties, `let` vs. `var`, and struct initializers.
- Optionals — `Int?`, `guard let`, `if let`, and nil-coalescing with `??`.
- Enums — `enum` cases and raw values (`enum Grade: String`).
- Functions — parameters, return types, and calling one function from another.
- Collection basics — `Array`, `for-in`, and passing an `[Student]` into a function.
Project Structure
The program lives in a single file, `main.swift`. `Student` is a plain data struct with no behavior of its own. `Grade` is a raw-value enum — a closed, compiler-checked set of the only letter grades that can ever exist in this program, which rules out an entire category of bugs a plain `String` field could introduce (typos like `"Aplus"` simply cannot compile as a `Grade` case). `gradeForMarks(_:)`, `summary(for:)`, `classAverage(_:)`, and `printReport(_:)` are free functions — none of them need to be methods on `Student`, because none of them depend on any single instance's private state; they each just take the data they need as a parameter and return a result.
Step 1: Define the Student Struct With Optional Marks
`marks` is typed `Int?`, not `Int`, because `nil` here does not mean zero — it means this student has not been graded yet. Modeling the "ungraded" state as a real Optional, instead of a magic number, is what forces the rest of the program to handle it explicitly instead of accidentally treating an ungraded student as having scored 0.
// marks is a genuine Optional here — nil doesn't mean "zero," it means "this// student hasn't been graded yet." Modeling that as Int? instead of using a// sentinel value like -1 is what forces every piece of code that reads marks// to explicitly decide what "ungraded" means for that operation.struct Student { let name: String let marks: Int? // nil = not yet graded; 0...100 = an actual recorded score}Step 2: Define the Grade Enum
A raw-value enum gives each case a fixed underlying value — here, a `String` — that can be read back with `.rawValue`, which is exactly what a display label needs. Using an `enum` instead of a plain `String` field makes "what are the valid grades" a closed set the compiler checks, rather than an open-ended collection of string literals that could drift or typo anywhere they are used.
// A raw-value enum: each case carries a fixed String ("A+", "A", ...) read// back with .rawValue. An enum is the right tool here instead of a plain// String, because it makes "what are the valid grades" a closed,// compiler-checked set instead of an open-ended set of magic strings.enum Grade: String { case aPlus = "A+" case a = "A" case b = "B" case c = "C" case fail = "Fail"}Step 3: Map a Score to a Grade
`gradeForMarks(_:)` is a free function, not a method on anything, because it has no dependency on instance state — it just takes a score and returns a `Grade`. Swift's `switch` supports matching against numeric ranges directly: `90...100` is a closed range including both endpoints, while `75..<90` is a half-open range that excludes 90, which is exactly what is needed so the boundaries between grades never overlap or leave a gap.
// A plain function: it takes a score and returns a Grade with no side// effects and no dependency on any instance state, so it has no reason to// live on Student or Grade as a method.func gradeForMarks(_ marks: Int) -> Grade { switch marks { case 90...100: return .aPlus // Closed range: 90 through 100 inclusive case 75..<90: return .a // Half-open range: 75 up to (not including) 90 case 60..<75: return .b case 40..<60: return .c default: return .fail // Anything else, including an invalid score below 0, counts as a fail }}Click Run to see what this code prints.
Step 4: Summarize One Student With guard let
`guard let` is preferred here over `if let` because the "no marks" case is the exceptional path that should return its own message immediately — everything after the `guard` statement can then safely assume `marks` is a real, unwrapped `Int`, with no further nil-checking needed for the rest of the function.
// guard let is preferred over if let here because the "no marks" case is the// exceptional path that should exit early with its own message — the rest of// the function can then safely assume marks is a real, unwrapped Int.func summary(for student: Student) -> String { guard let marks = student.marks else { return "\(student.name): No marks recorded yet" } let grade = gradeForMarks(marks) return "\(student.name): \(marks)/100 -> Grade \(grade.rawValue)"}Step 5: Compute the Class Average With compactMap
`compactMap` runs its closure over every element and keeps only the results that are not `nil`, unwrapping them in the same step — "give me every marks value that actually exists" in one line, instead of a manual loop with an `if let` and an accumulator array. `reduce(0, +)` then folds that array of unwrapped scores down into a single sum before dividing by the count.
// compactMap runs the closure over every element and keeps only the non-nil// results, unwrapping them in the process — exactly "give me every marks// value that actually exists," in one line instead of a manual loop with an// if-let and an accumulator array.func classAverage(_ students: [Student]) -> Double { let recorded = students.compactMap { $0.marks } guard !recorded.isEmpty else { return 0 } // Avoid dividing by zero when nobody has been graded yet let total = recorded.reduce(0, +) // reduce folds the array into a single sum return Double(total) / Double(recorded.count)}
func printReport(_ students: [Student]) { print("\n--- Grade Report ---") for student in students { print(summary(for: student)) } let average = classAverage(students) print(String(format: "Class average (graded students only): %.1f", average))}Step 6: Build the Menu Loop
When adding a student, an empty or unreadable marks line is turned into `nil` explicitly with a ternary, rather than trying (and failing) to parse it as a number — `Int("")` also returns `nil`, so both "left blank" and "typed something non-numeric" naturally collapse into the same "not graded yet" state that `Student.marks` already models.
var students: [Student] = []var running = true
while running { print("""
===== STUDENT GRADING SYSTEM ===== 1. Add Student 2. Generate Report 3. Exit """) print("Enter your choice: ", terminator: "") guard let choiceLine = readLine(), let choice = Int(choiceLine) else { print("Invalid input, try again.") continue }
switch choice { case 1: print("Enter student name: ", terminator: "") let name = readLine() ?? "Unnamed Student" // Nil-coalescing: a missing line still produces a usable student print("Enter marks, or leave blank if not graded yet: ", terminator: "") let marksLine = readLine() ?? "" // Int("") returns nil, and so does Int() on anything non-numeric — both // naturally become "not graded yet" with no extra special-casing needed. let marks = marksLine.isEmpty ? nil : Int(marksLine) students.append(Student(name: name, marks: marks)) print("Student added.") case 2: printReport(students) case 3: print("Goodbye!") running = false default: print("Invalid choice, try again.") }}Complete Code
Here is the full program assembled in the correct order, ready to save as `main.swift` and run with `swift main.swift`, or paste into a Swift Playground.
struct Student { let name: String let marks: Int?}
enum Grade: String { case aPlus = "A+" case a = "A" case b = "B" case c = "C" case fail = "Fail"}
func gradeForMarks(_ marks: Int) -> Grade { switch marks { case 90...100: return .aPlus case 75..<90: return .a case 60..<75: return .b case 40..<60: return .c default: return .fail }}
func summary(for student: Student) -> String { guard let marks = student.marks else { return "\(student.name): No marks recorded yet" } let grade = gradeForMarks(marks) return "\(student.name): \(marks)/100 -> Grade \(grade.rawValue)"}
func classAverage(_ students: [Student]) -> Double { let recorded = students.compactMap { $0.marks } guard !recorded.isEmpty else { return 0 } let total = recorded.reduce(0, +) return Double(total) / Double(recorded.count)}
func printReport(_ students: [Student]) { print("\n--- Grade Report ---") for student in students { print(summary(for: student)) } let average = classAverage(students) print(String(format: "Class average (graded students only): %.1f", average))}
var students: [Student] = []var running = true
while running { print("""
===== STUDENT GRADING SYSTEM ===== 1. Add Student 2. Generate Report 3. Exit """) print("Enter your choice: ", terminator: "") guard let choiceLine = readLine(), let choice = Int(choiceLine) else { print("Invalid input, try again.") continue }
switch choice { case 1: print("Enter student name: ", terminator: "") let name = readLine() ?? "Unnamed Student" print("Enter marks, or leave blank if not graded yet: ", terminator: "") let marksLine = readLine() ?? "" let marks = marksLine.isEmpty ? nil : Int(marksLine) students.append(Student(name: name, marks: marks)) print("Student added.") case 2: printReport(students) case 3: print("Goodbye!") running = false default: print("Invalid choice, try again.") }}Sample Run
Click Run to see what this code prints.
Extend This Project
- Add a `Subject` enum and track per-subject marks in a `[Subject: Int]` dictionary instead of one overall score.
- Add a `retest(name:newMarks:)` function that replaces a student's marks and re-runs the report.
- Persist the roster to a JSON file with `Codable` so grades survive between runs.
- Add a `topStudent(_:)` function using `max(by:)` over students with non-nil marks.
- Give `Grade` a computed `var passing: Bool` property and report a pass/fail count alongside the average.
Summary
You built a grading system where "no marks yet" is a real, type-checked state instead of a magic number, and `guard let` turns handling that state into an explicit early exit rather than a hidden edge case. The `Grade` enum kept every possible letter grade to a closed, compiler-verified set, and `compactMap` let the class average skip ungraded students without a single manual nil-check in the averaging logic itself. That combination — optionals for "might not exist," enums for "one of a fixed set," and small functions that lean on both — is the backbone of idiomatic Swift data processing.