Skip to content

↑ R1 · ← Types & Missingness · → Environments & Scoping

🎯 Mục tiêu

Mục tiêu của bài này không phải “thuộc cú pháp if/for”, mà là viết code R có thể kiểm thử, tái sử dụng, và debug nhanh. Trong production, đó là thứ quyết định bạn có ngủ ngon hay không.

1) Control flow: dùng đúng chỗ

1.1 if/else: điều kiện phải là TRUE/FALSE thật sự

if chỉ nhận 1 giá trị logical. NA sẽ crash, vector logical sẽ bị lấy phần tử đầu (dễ bug).

r
flag <- NA
if (flag) "ok"   # Error

x <- c(TRUE, FALSE)
if (x) "ok"      # Warning + dùng x[1]

Guardrails thực dụng:

r
if (isTRUE(flag)) {
  "ok"
} else {
  "not ok"
}

1.2 for: tốt cho orchestration, không phải để “tính toán thay vectorization”

for hợp cho:

  • gọi API theo batch,
  • đọc nhiều file,
  • orchestrate pipeline,
  • log progress.

Không hợp để làm toán element-wise khi bạn có thể dùng vectorization. Lý do: code dài, dễ state leak, khó test.

1.3 while: chỉ dùng khi điều kiện dừng rõ và có “safety cap”

while dễ tạo infinite loop. Trong production, luôn có giới hạn:

r
max_iters <- 1000L
i <- 0L
while (i < max_iters) {
  i <- i + 1L
}
stopifnot(i == max_iters)

⚠️ Cạm bẫy

Đừng viết while(TRUE) nếu không có timeout/backoff/giới hạn số lần retry.

2) Vectorization vs loops: tiêu chí chọn

Vectorization không phải “thần chú”, nó là trade-off.

2.1 Khi nên vectorize

  • Transform theo cột (per-element computation) → vectorize.
  • Filter/mutate/summarise dạng bảng → ưu tiên style “pipeline”.

2.2 Khi nên loop

  • Mỗi iteration là một “unit of work” có I/O (API, file, DB).
  • Cần early stop/break rõ ràng theo điều kiện.
  • Cần retry/backoff.

Rule of thumb: logic thuần (pure) → vectorize; orchestration/I/O → loop.

3) Functions: thiết kế để “pure-ish” và test được

“Pure-ish” nghĩa là:

  • function nhận input rõ ràng (args),
  • trả output rõ ràng (return),
  • side-effects (đọc/ghi file, set seed, set options) đặt ở entrypoint.

3.1 Input validation: fail fast

Validation ở đầu function giúp giảm bug lan truyền.

r
assert_has_cols <- function(df, required_cols) {
  stopifnot(is.data.frame(df))
  stopifnot(is.character(required_cols))
  missing_cols <- setdiff(required_cols, names(df))
  if (length(missing_cols) > 0) {
    stop(paste("Missing columns:", paste(missing_cols, collapse = ", ")))
  }
  invisible(TRUE)
}

3.2 Return contract: nói rõ kiểu và shape

Return contract tối thiểu:

  • trả về kiểu gì (vector/list/data.frame)?
  • nếu data.frame: có cột nào, có bao nhiêu dòng (có thể thay đổi không)?
  • có thể trả length 0 không?

Ví dụ function trả về data.frame đã chuẩn hóa:

r
normalize_events <- function(events_df) {
  assert_has_cols(events_df, c("user_id", "event_time", "event_type"))
  events_df[order(events_df$user_id, events_df$event_time), , drop = FALSE]
}

3.3 Side-effects: phân tầng rõ

Tách code thành 3 lớp:

  • Pure logic: R/ (functions)
  • Orchestrator: scripts/ (I/O, args, logging, seed)
  • Presentation: reports/ (render/plots, ít logic)

4) Mẫu “ship-ready”: script gọi functions

Mẫu entrypoint tối thiểu (tư duy, không bắt buộc cấu trúc thư mục):

r
args <- commandArgs(trailingOnly = TRUE)
input_path <- args[[1]]
output_path <- args[[2]]

events_df <- read.csv(input_path)
events_df <- normalize_events(events_df)
write.csv(events_df, output_path, row.names = FALSE)

Điểm quan trọng:

  • Orchestrator xử lý đường dẫn, I/O.
  • Logic nằm trong function để test.

5) Checklist: để code “test được”

✅ Checklist triển khai

Khi viết function

  • Input explicit, không đọc global state
  • Validate args, fail fast
  • Return contract rõ (kiểu + shape)
  • Không đổi options()/timezone/locale bên trong (hoặc đổi thì reset)

Khi viết loop/orchestration

  • Có max retries/timeout
  • Log context (file/batch id)
  • Dễ resume (idempotent output nếu có thể)