পাঠ ১৭.১

Future এবং Async syntax

Futures and the Async Syntax

Async programming-এর মূল building block হলো Future — একটা value যেটা এখন ready না, কিন্তু পরে কোনো একসময় ready হবে। Rust-এ async এবং await দু'টো keyword দিয়ে আমরা future-গুলোর সাথে কাজ করি। এই পাঠে আমরা প্রথম async program লিখব এবং দেখব Rust internally এই syntax-কে state machine-এ কিভাবে transform করে।

প্রথম async program

চলো একটা ছোট CLI বানাই — একটা URL নিয়ে সেটার HTML page-এর <title> element fetch করবে। শুরুতে নতুন project আর trpl crate add করো:

$ cargo new hello-async
$ cd hello-async
$ cargo add trpl

trpl ("The Rust Programming Language"-এর shorthand) এই বইয়ের example-গুলো simple করার জন্য বানানো — internally এটা futures এবং tokio crate-এর key type/trait/function-গুলো re-export করে। tokio Rust ecosystem-এর সবচেয়ে widely used async runtime।

page_title function define করা

src/main.rsrust
use trpl::Html;

async fn page_title(url: &str) -> Option<String> {
    let response = trpl::get(url).await;
    let response_text = response.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

লক্ষ্য করার মতো কয়েকটা জিনিস:

  • async fn — function-কে async হিসেবে mark করে; এর body interrupt এবং resume হতে পারে।
  • .await postfix — expression-এর পরে আসে। এটা পেলে runtime current future-কে pause করে control return নেয়, future ready হলে আবার resume।
  • Postfix syntax-এর সুবিধা — method chain হলে অনেক পরিষ্কার পড়া যায়।

দু'টো .await-কে chain করে আরও compact করা যায়:

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

async main কাজ করে না

সরাসরি main-কে async বানিয়ে দেখো —

async fn main() {
    let args: Vec<String> = std::env::args().collect();
    let url = &args[1];
    match page_title(url).await {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
}
compile errortext
error[E0752]: `main` function is not allowed to be `async`
 --> src/main.rs:6:1
  |
6 | async fn main() {
  | ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`

কেন? Async code execute করতে একটা runtime দরকার যেটা future-গুলো poll করে, schedule করে, sleep handle করে। Rust নিজে কোনো runtime ship করে না — কোনটা ব্যবহার করবে সেটা তোমার choice (tokio, async-std, smol ইত্যাদি)। তাই main-এর কাজ runtime-কে initialize করা, তারপর সেই runtime-এ async code চালানো।

Async function = Future-return করা function

Compiler-এর দৃষ্টিতে async fn আসলে এমন একটা function যেটা একটা Future return করে। অর্থাৎ এই দু'টো equivalent:

use std::future::Future;
use trpl::Html;

fn page_title(url: &str) -> impl Future<Output = Option<String>> {
    async move {
        let text = trpl::get(url).await.text().await;
        Html::parse(&text)
            .select_first("title")
            .map(|title| title.inner_html())
    }
}

Body-টা একটা async move block-এ wrap হয়। Return type impl Future<Output = Option<String>> — অর্থাৎ এটা একটা future, যেটা শেষমেশ Option<String> দেবে।

block_on দিয়ে runtime চালানো

src/main.rsrust
use trpl::Html;

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::block_on(async {
        let url = &args[1];
        match page_title(url).await {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
    })
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html())
}

trpl::block_on একটা future নেয়, runtime চালু করে, future complete না হওয়া পর্যন্ত block করে। এটা synchronous কোডের সাথে async কোডের bridge।

$ cargo run -- "https://www.rust-lang.org"
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
            Rust Programming Language

আড়ালে state machine

Compiler প্রতিটা .await-কে state machine-এর একটা state-এ পরিণত করে। Conceptually আমাদের page_title-এর জন্য enum কিছুটা এমন:

enum PageTitleFuture<'a> {
    Initial { url: &'a str },
    GetAwaitPoint { url: &'a str },
    TextAwaitPoint { response: trpl::Response },
}

প্রতিটা .await point একটা place যেখানে control runtime-কে ফিরিয়ে দেওয়া হয়। সাধারণ borrowing/ownership rule এখানেও apply। ভালো কথা — এই state machine compiler নিজেই generate করে দেয়, আমাদের লিখতে হয় না।

দু'টো URL-এর race

Async-এর আসল মজা concurrent execution-এ। দু'টো URL-এ একসাথে request পাঠাই — যেটা আগে response দেবে সেটাই declare করি winner:

src/main.rsrust
use trpl::{Either, Html};

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::block_on(async {
        let title_fut_1 = page_title(&args[1]);
        let title_fut_2 = page_title(&args[2]);

        let (url, maybe_title) =
            match trpl::select(title_fut_1, title_fut_2).await {
                Either::Left(left) => left,
                Either::Right(right) => right,
            };

        println!("{url} returned first");
        match maybe_title {
            Some(title) => println!("Its page title was: '{title}'"),
            None => println!("It had no title."),
        }
    })
}

async fn page_title(url: &str) -> (&str, Option<String>) {
    let response_text = trpl::get(url).await.text().await;
    let title = Html::parse(&response_text)
        .select_first("title")
        .map(|title| title.inner_html());
    (url, title)
}

trpl::select দু'টো future নেয় — যেটা আগে complete হয় তার output return করে। Result-এর type Either<A, B> — পরিচিত enum pattern:

enum Either<A, B> {
    Left(A),
    Right(B),
}

Either::Left মানে first future আগে শেষ হয়েছে, Right হলে second। এখানে winner/loser-এর কোনো ধারণা নেই — দু'টোই legitimate result, আমরা শুধু জানতে চাই কোনটা আগে।

এই পাঠ থেকে যা শিখলে

  • Future — এমন value যা পরে ready হবে; async block/function-ই future তৈরি করে।
  • Future-গুলো lazy.await না করলে কিছুই হয় না।
  • Postfix .await — chaining-এর জন্য convenient; প্রতিটা await point runtime-এ control হস্তান্তর।
  • main async হতে পারে না — runtime আলাদাভাবে launch করতে হয়। trpl::block_on সেই কাজটা করে।
  • Compiler আড়ালে state machine generate করে — প্রতিটা await একটা state।
  • trpl::select + Either — দু'টো future-এর মধ্যে যেটা আগে শেষ হয় তার response ব্যবহার।