/*:#localized(key: "VersionBModern")
 # Version B (modern): `async`/`await` + `async let`

 Same task. Each fetch is now an **async function** that either returns a
 value or `throws`. We start both with **`async let`** so they run
 concurrently, `await` both together, combine, and let `@MainActor` put us on
 the main thread, no manual hop.
 */
//#-editable-code
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

enum BarError: Error {
    case offline
}

func fetchMenu() async throws -> [String] {
    try await Task.sleep(for: .milliseconds(600))   // network wait
    return ["Latte", "Cold brew", "Espresso"]
}

func fetchStock() async throws -> [String: Int] {
    try await Task.sleep(for: .milliseconds(400))   // network wait
    return ["Latte": 3, "Cold brew": 0, "Espresso": 8]
}

@MainActor
func showSellableMenu() async {
    do {
        async let menu = fetchMenu()     // both start now,
        async let stock = fetchStock()   // in parallel
        let (drinks, levels) = try await (menu, stock)
        let sellable = drinks.filter { (levels[$0] ?? 0) > 0 }
        print("Sellable today: \(sellable)")
    } catch {
        print("Couldn't build the menu: \(error)")
    }
}

print("Fetching menu and stock...")
Task {
    await showSellableMenu()
    PlaygroundPage.current.finishExecution()
}
//#-end-editable-code
/*:#localized(key: "VersionBModern-2")
 **Watch it run:** identical output:

 * callout(Output): 
 `Fetching menu and stock...`  
 `Sellable today: ["Latte", "Espresso"]`

 And the timing is identical too (both still run in parallel, ~0.6s total).
 The Playground page keeps running while the `Task` completes.

 * callout(Try it): Bump `fetchMenu`'s sleep to `.milliseconds(1200)`: the
 total time follows the *slowest* fetch, not the sum of both. Which line still
 prints first?

 Now look at what disappeared:

 * **No shared mutable variables.** `menu` and `stock` are just values.
 * **No `enter`/`leave`.** `async let` starts each child; `try await (menu, stock)`
 is the join. There is no counter to balance.
 * **One error path.** Either `fetchMenu` or `fetchStock` throwing lands in the
 single `catch`. You didn't unwrap two optionals, and you didn't call `.get()`.
 * **No manual main-thread hop.** `@MainActor` on the function *is* the
 guarantee. The combine and the `print` run on the main actor because the
 function is isolated to it. Forgetting to return to main is no longer
 possible.

 The logic, the `filter`, is now the most prominent line in the function,
 because everything around it got out of the way.

 ---
 
 ## Feel the difference

 * **Shared mutable state.** Classic: two `Result?` variables. Modern: none.
 * **"Wait for both."** Classic: `DispatchGroup` with `enter`/`leave`. Modern:
 `async let` with `await`.
 * **Error handling.** Classic: unwrap optionals, then `try`/`catch`. Modern:
 one `try`/`catch`.
 * **Main-thread delivery.** Classic: remembered `notify(queue: .main)`.
 Modern: `@MainActor`, automatic.
 * **Lines that aren't the actual task.** Classic: many. Modern: few.
 * **Compiler catches a missed main hop.** Classic: no. Modern: yes.

 Same behavior, same parallelism, same output, but B says what it *means* and
 lets the compiler enforce the parts you used to hold in your head. That's the
 whole argument for the modern model in one example.

 [Previous](@previous)  |  page 4 of 8  |  [Next: Classic and Modern in Practice](@next)
 */
