Giao diện
↑ 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:
listlà dữ liệu.environmentlà “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ự:
- local scope của
f(args + local variables) - environment nơi
fđược định nghĩa - 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 gNế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 >= thresholdthreshold 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)và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ỏ