/*:#localized(key: "ClassicModernInPractice")
 # Classic and modern in practice

 ## The concept map

 Almost every classic tool has a direct modern counterpart. When you read old
 code, or migrate it, this is the translation table:

 * A completion handler, `(Result<T,E>) -> Void`, ➡️ an `async throws -> T`
 function. The result comes *back* as a return value; errors `throw`.
 * `DispatchQueue.global().async { }` ➡️ `Task { }` / the cooperative pool. You
 describe *work*, not *which queue*.
 * `DispatchGroup` / `semaphore.wait()` for a batch ➡️ `async let` (fixed set) or
 `TaskGroup` (dynamic set). The join is structural; no counter to balance.
 * A serial queue used to guard shared state ➡️ an **`actor`**. Isolation is
 enforced by the type, not by discipline.
 * `DispatchQueue.main.async { }` ➡️ **`@MainActor`**. Main-thread-ness is part
 of the signature.
 * `Operation` with manual `isCancelled` checks ➡️ cooperative **`Task`
 cancellation** (`Task.isCancelled` / `try Task.checkCancellation()`).
 Cancellation propagates down the task tree automatically.
 * A delegate or callback that fires many times ➡️ **`AsyncStream`** (or a
 **continuation** for one-shot). Events become a `for await` loop you can
 `break` out of.

 Two things worth underlining:

 First, **`async let` and `TaskGroup` both replace `DispatchGroup`**: pick
 `async let` when you know the fetches up front (like our two here), and
 `TaskGroup` when the number is decided at runtime.

 Second, **the actor is the quiet hero**: the single most bug-prone thing in
 the classic model (a serial queue or a count-1 semaphore protecting shared
 state that you had to remember to route every access through) becomes a
 type the compiler polices for you.

 ---

 ## When to still use the classic model

 The modern model is the default for new code. But "classic is dead" would be
 wrong and would steer you into needless rewrites. Reach for (or keep) GCD and
 `Operation` when:

 * **You're calling older APIs.**  
 Plenty of frameworks (and your own older code) still hand you completion
 handlers or delegates. You *interoperate* with them; you don't have to convert
 the whole world first. (The clean move is to wrap them; see the next page.)
 * **You need very fine-grained dispatch control.**  
 A specific custom serial queue with a precise QoS, a `DispatchSource` for a
 file or timer, a barrier block on a concurrent queue. These are still
 legitimate, and sometimes the most direct tool. The cooperative pool
 deliberately hides thread details; when you truly need those details, GCD is
 right there.
 * **You target OS versions predating `async`/`await`.**  
 Swift concurrency runs on iOS 13 and later (and the equivalent macOS 10.15,
 tvOS 13, watchOS 6). Since iOS 13 shipped in 2019, this reason is now rare, but
 if a deployment target still reaches below that line, GCD is your portable
 option.
 * **The code already works and you haven't migrated it yet.**  
 Working GCD is not a bug. Rewriting stable code purely to modernize it adds
 risk for no user-facing gain. Migrate where you're already changing things,
 not everywhere at once.

 Being honest about this matters: the goal is *correct, maintainable* code, not
 a 100% modern codebase for its own sake.

 [Previous](@previous)  |  page 5 of 8  |  [Next: A Pragmatic Migration Strategy](@next)
 */
