Skip to content

↑ R1 · ← Control Flow & Functions · → Tidyverse Mindset

🎯 Mục tiêu

Nếu bạn từng gặp bug kiểu “hôm qua chạy đúng, hôm nay chạy sai” hoặc “hàm này tự nhiên dùng nhầm biến ở đâu đó”, khả năng cao là bạn chưa nắm environment + scoping. Đây là bẫy senior vì bug thường không nằm ở dòng lỗi, mà nằm ở mô hình lookup.

1) Environments: object chứa bindings (name → value)

Environment là một map: tên biến → object. Mỗi environment có:

  • parent environment (để lookup theo chuỗi),
  • danh sách bindings,
  • và có thể bị mutate theo runtime.

Tư duy đúng:

  • list là dữ liệu.
  • environment là “không gian tên” + cơ chế lookup.

1.1 Vì sao environments quan trọng

  • Package namespaces là environments.
  • Search path của R (global, packages, base, ...) là chain environments.
  • Closure giữ reference tới environment khi function được tạo.

1.2 Nhìn nhanh “function sống ở đâu”

r
f <- function(x) x + 1
environment(f)

environment(f) là nơi R sẽ tìm các free variables của f.

2) Lexical scoping: R lookup biến theo nơi function được định nghĩa

R dùng lexical scoping: biến được tìm trong environment của function (và parent chain), không phải nơi gọi (dynamic scoping).

2.1 Lookup rule (mô hình mental)

Khi chạy f(), R tìm biến theo thứ tự:

  1. local scope của f (args + local variables)
  2. environment nơi f được định nghĩa
  3. parent chain cho tới base

2.2 Ví dụ “ăn nhầm biến” (shadowing)

r
x <- 10
f <- function() x + 1
f()  # 11

g <- function() {
  x <- 100
  f()
}
g()  # vẫn 11, vì f không nhìn thấy x trong g

Nếu bạn kỳ vọng g() trả 101 thì bạn đang nghĩ theo dynamic scoping.

⚠️ Cạm bẫy

Bug production hay gặp: dùng biến global vô tình. Code chạy đúng trên máy tác giả (vì global env có sẵn biến), nhưng fail trên CI/production.

2.3 Free variables là “dependency ẩn”

r
threshold <- 0.8
is_good <- function(score) score >= threshold

threshold là dependency nhưng không nằm trong args. Điều này làm:

  • test khó,
  • behavior khó trace,
  • refactor nguy hiểm.

Rule thực dụng: biến nào ảnh hưởng output → ưu tiên đưa vào args hoặc config object truyền vào.

3) Assignment: <- vs <<- (và vì sao <<- là bẫy)

3.1 <- gán vào environment hiện tại

Trong function, <- tạo/ghi local variable (trừ khi dùng assign() với envir cụ thể).

3.2 <<- gán lên parent chain

<<- tìm tên biến trong parent chain và gán vào nơi tìm thấy (hoặc tạo ở global nếu không tìm thấy).

r
counter_factory <- function() {
  i <- 0L
  function() {
    i <<- i + 1L
    i
  }
}

Ở đây <<- hợp lý vì state nằm trong closure environment (không phải global). Nhưng <<- trong code business thông thường rất dễ biến thành “global mutation”.

⚠️ Cạm bẫy

Nếu <<- vô tình đẩy state lên global env, bạn sẽ có bug phụ thuộc thứ tự chạy (order-dependent), rất khó debug.

4) Closures: use-case thực (đúng chỗ, không overengineer)

Closure = function + environment captured.

4.1 Function factory (inject config)

Thay vì free variable:

r
make_is_good <- function(threshold) {
  stopifnot(is.numeric(threshold), length(threshold) == 1)
  function(score) score >= threshold
}

is_good_08 <- make_is_good(0.8)
is_good_08(c(0.7, 0.9))

Ưu điểm:

  • dependency explicit (threshold),
  • test dễ (inject threshold),
  • không bị “ăn nhầm biến” từ global.

4.2 Stateful function (có kiểm soát)

Counter là ví dụ hợp lệ. Use-case thực tế:

  • rate limiter đơn giản,
  • cache nhỏ trong memory (khi bạn hiểu lifecycle),
  • debug instrumentation (đếm số lần gọi).

Nguyên tắc: state phải nằm trong closure env, không viết ra global.

4.3 Closure trong pipeline: cẩn thận capture loop variable

Nếu bạn tạo danh sách functions trong vòng lặp và “capture” biến thay đổi, hãy kiểm soát capture bằng cách bind biến cục bộ.

r
make_adders <- function(xs) {
  lapply(xs, function(xi) {
    function(y) y + xi
  })
}

Ở đây xi là local của từng iteration (an toàn).

5) Debug checklist cho scoping bugs

✅ Checklist triển khai

Khi nghi ngờ “scoping bug”

  • Tìm free variables trong function (tên dùng nhưng không có trong args/local)
  • In environment(f)parent.env(...) để hiểu chain lookup
  • Tránh phụ thuộc global env: đưa dependency vào args/config
  • Tách orchestration (I/O) khỏi logic thuần để test bằng data nhỏ