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
Studentstruct with a genuinely optionalmarks: Int?field. - A
Gradeenum with rawStringvalues mapping scores to letter grades. - A
gradeForMarks(_:)function using aswitchover numeric ranges. - A
summary(for:)function that usesguard letto handle ungraded students. - A
classAverage(_:)function usingcompactMapandreduceto average only recorded scores. - A
readLine()-driven menu loop for adding students and generating the report.
Prerequisites
- Structs — stored properties,
letvs.var, and struct initializers. - Optionals —
Int?,guard let,if let, and nil-coalescing with??. - Enums —
enumcases 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
Subjectenum 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
Codableso grades survive between runs. - Add a
topStudent(_:)function usingmax(by:)over students with non-nil marks. - Give
Gradea computedvar passing: Boolproperty 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.