1 tháng học Rust - Closure

1 tháng học Rust - Closure

Tin ngắn hàng ngày dành cho bạn
  • Rust sinh ra để tái định nghĩa nhiều thứ. Trong đó chắc phải kể đến JavaScript. Từ đầu năm đến giờ các công cụ làm từ Rust mà để cho JavaScript dùng đếm sương sương cũng vài ba cái rồi. Mới đây nhất là Oxc.

    Oxc là một công cụ phân tích cú pháp (parser), kiểm tra lỗi (lint), định dạng (formatter), chuyển đổi (transformer), minifier... tất cả đều được viết bằng Rust, trong một công cụ duy nhất.

    Mặc dù vẫn đang trong quá trình xây dựng nhưng thử nhìn điểm hiệu năng của nó so với swc hoặc eslint mà xem 🫣

    » Xem thêm
  • Mấy hôm nay, à mà cũng không hẳn, do sự kiện WWDC vừa rồi nên Apple lại bị dân cư mạng mang ra bàn tán rằng rốt cục thì các tính năng AI của họ đang ở đâu? Trong khi các hãng khác đang lao mình vào việc mang AI lên thiết bị, phần mềm của họ thì Apple lại đang có vẻ... không quan tâm lắm.

    Thậm chí mới đây các nhà nghiên cứu của Apple cho rằng các mô hình LLM sẽ "sụp đổ hoàn toàn về độ chính xác" khi được đưa ra các vấn đề cực kỳ phức tạp. Chỉ ra rằng suy luận chỉ là huyễn hoặc thì ngay lập tức đã có nhiều bài phản bác nghiên cứu này. Một lần nữa cho thấy rằng Apple đang suy nghĩ điều gì với AI trên thiết bị của họ?

    Mình thì nghĩ đơn giản thôi, Apple có vẻ đang gặp khó khăn với việc tạo ra AI cho riêng họ. Tức là khó khăn ngay từ đoạn thu thập dữ liệu để đào tạo rồi. Họ luôn tỏ ra tôn trọng quyền riêng tư của người dùng nên chẳng lẽ lại lên mạng đi xào nấu dữ liệu ở khắp nơi, hoặc "chôm" dữ liệu dưới máy người dùng lên? Chắc chắn, họ cũng không muốn cung cấp thêm dữ liệu người dùng cho các bên thứ 3 như OpenAI.

    Nhưng nhờ những khó khăn này biết đâu họ lại tìm ra được hướng đi mới. Ai cũng chọn phần dễ thì gian khổ để phần cho ai 😁. À mình không phải là "fan" của Apple, chỉ là thấy cái nào phù hợp thì dùng thôi 🤓

    » Xem thêm
  • Người "nhạy cảm" với markdown đó là khi thấy một thư viện tạo khung soạn thảo mới là nhảy ngay vào xem nó có gì mới. Milkdown/milkdown là một ví dụ.

    Xem thử thì thấy ổn phết mọi người ạ. Vài nữa thử tích hợp vào opennotas xem sao. Mang tiếng là ứng dụng ghi chú hỗ trợ markdown cơ mà cái thư viện tiptap nó không chịu làm thêm phần hỗ trợ markdown 😩. Dùng thư viện ngoài thì vẫn chưa ngon cho lắm.

    » Xem thêm

Vấn đề

Trong JavaScript, Closure để chỉ một hàm có thể nhớ và truy cập các biến trong phạm vi bên ngoài của nó, ngay cả sau khi phạm vi bên ngoài đó đã kết thúc. Nói ngắn gọn, khi một hàm được định nghĩa bên trong một hàm khác, nó truy cập các biến từ hàm cha thì closure được tạo ra.

Ví dụ:

function makeCounter() {
  let count = 0; // biến trong phạm vi của makeCounter

  return function() {
    count++; // closure truy cập biến count
    return count;
  };
}

const counter1 = makeCounter(); // Tạo closure
console.log(counter1()); // 1
console.log(counter1()); // 2

count chỉ tồn tại trong makeCounter, nhưng hàm trả về (return) vẫn truy cập được nó. Bằng chứng là mỗi lần gọi counter1() thì count tăng dần.

Trong Rust, closure cũng tương tự như trong JavaScript, nó có thể ghi nhớ và sử dụng biến từ phạm vi bên ngoài nơi nó được định nghĩa. Tuy vậy, closure trong Rust phức tạp hơn JavaScript rất nhiều.

Closure trong Rust

Closure trong Rust là các hàm ẩn danh mà bạn có thể lưu trong một biến hoặc truyền dưới dạng đối số cho các hàm khác. Bạn có thể tạo closure ở một nơi rồi gọi closure ở nơi khác.

Không giống như các hàm, closure có thể nắm bắt các giá trị từ phạm vi mà chúng được định nghĩa. Lấy một ví dụ để so sánh giữa hàm với closure.

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    println!("{}", add(1, 2));
}

Để gọi add, buộc phải truyền vào 2 tham số ab, hàm trả về tổng của 2 số đó.

Trong khi đó closure được viết như sau.

fn main() {
    let a = 1;
    let add = |b| a + b;

    println!("{}", add(2)); // 3
}

add là một hàm closure, khi gọi add nó có thể truy cập được vào biến a từ môi trường bên ngoài.

Closure có thể được khai báo bằng cách gán vào một biến hoặc gọi trực tiếp từ nơi thực thi. |b| a + b là cách viết rút gọn của closure. Cách viết đầy đủ trông giống như:

fn main() {
  let a = 1;
  let add = |b: u32| -> u32 {
    a + b
  };

  println!("{}", add(2)); // 3
}

Quyền sở hữu

Closure có thể nắm bắt các giá trị từ môi trường của chúng theo ba cách:

  • Mượn bất biến.

  • Mượn có thể thay đổi.

  • Chiếm quyền sở hữu.

Closure sẽ quyết định sử dụng cách nào trong số này dựa trên phần xử lý trong thân hàm closure.

Ví dụ.

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let only_borrows = || println!("From closure: {list:?}");

    println!("Before calling closure: {list:?}");
    only_borrows();
    println!("After calling closure: {list:?}");
}

only_borrows chỉ đơn giản là in ra danh sách trong list, vì vậy closure này chỉ mượn bất biến.

Xem tiếp ví dụ dưới đây.

fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {list:?}");
}

borrows_mutably gọi một hàm push vào list, nó làm thay đổi list ban đầu cho nên closure này mượn có thể thay đổi.

Closure chiếm quyền sở hữu như ở trong ví dụ dưới đây.

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    thread::spawn(move || println!("From thread: {list:?}"))
        .join()
        .unwrap();
}

Fn, FnMut, FnOnce

Sau khi closure đã nắm bắt được tham chiếu hoặc nắm bắt được quyền sở hữu giá trị từ môi trường nơi closure được định nghĩa, mã trong phần thân của closure sẽ xác định những gì xảy ra với các tham chiếu hoặc giá trị khi closure được chạy. Closure có thể thực hiện bất kỳ hành động nào sau đây: di chuyển giá trị đã nắm bắt ra khỏi closure, thay đổi giá trị hoặc không gây ra ảnh hưởng gì cả.

Vì vậy có tổng cộng 3 kiểu closure triển khai trait là Fn, FnMutFnOnce tương ứng với 3 cách mà closure "đối xử" với quyền sở hữu của các giá trị từ môi trường mà nó nắm bắt: bất biến, có thể thay đổi và chiếm quyền.

Tại sao phải triển khai 3 kiểu này vì nó quyết định cách sử dụng closure trong một số trường hợp nhất định. Ví dụ phương thức sort_by_key dùng để sắp xếp lại danh sách chỉ có thể sử dụng clousure triển khai Fn.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{list:#?}");
}

Nếu cố tình sử dụng một closure có kiểu triển khai khác như FnMut thì chương trình không thể chạy được. Ví dụ chương trình dưới đây sẽ báo lỗi cannot move out of value, a captured variable in an FnMut closure.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    let value = String::from("closure called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{list:#?}");
}

Tổng kết

Closure trong Rust mặc dù tương tự về khái niệm với JavaScript nhưng lại phức tạp hơn rất nhiều. Nó cho phép nắm bắt giá trị từ phạm vi bên ngoài thông qua ba cách: mượn bất biến, mượn có thể thay đổi và chiếm quyền sở hữu, tùy thuộc vào cách sử dụng trong thân closure. Điều này được kiểm soát thông qua ba trait chính là Fn, FnMut, và FnOnce, mỗi loại phù hợp với các tình huống khác nhau. Điểm khác biệt này không chỉ giúp closure trong Rust linh hoạt hơn mà còn đảm bảo an toàn bộ nhớ và quản lý quyền sở hữu chặt chẽ, một yếu tố cốt lõi trong thiết kế của Rust.

Cao cấp
Hello

Tôi & khao khát "chơi chữ"

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!

Xem tất cả

Đăng ký nhận thông báo bài viết mới

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.

Bình luận (0)

Nội dung bình luận...