Vòng lặp for cơ bản
Vòng lặp (loop) for trong R chạy một khối lệnh một lần cho mỗi phần tử của một vector hoặc list. Biến lặp lần lượt nhận từng giá trị:
Ba phần tử, ba lượt lặp. Không cần khởi tạo bộ đếm, cũng không cần duy trì điều kiện: for (fruit in fruits) đọc là "với mỗi fruit trong fruits", và nó làm đúng như vậy. Cùng một dạng này lặp qua các phần tử của list, một dãy số (for (i in 1:10)), hoặc tên của bất cứ thứ gì qua names().
In bên trong vòng lặp cần print() hoặc cat()
Ở console, gõ x sẽ in ra giá trị của nó. Bên trong thân vòng lặp thì không: R chỉ tự động in ở cấp cao nhất, còn một biểu thức trơn trong vòng lặp được tính rồi bị bỏ đi:
Vì vậy khi một vòng lặp "chẳng làm gì cả", đây là điều đầu tiên cần kiểm tra. Dùng print() để xem nhanh một giá trị bất kỳ, hoặc cat() khi bạn tự ghép một dòng đầu ra (nhớ thêm "\n", vì cat() không tự xuống dòng).
Lặp theo chỉ số với seq_along()
Đôi khi bạn cần cả vị trí lẫn giá trị: để ghi vào một vector khác ở cùng chỉ số, hoặc để in một danh sách có đánh số. Lặp qua các chỉ số bằng seq_along():
Trong code cũ bạn sẽ gặp for (i in 1:length(prices)), và cách viết này ẩn chứa một bug thật sự. Khi vector rỗng, length() bằng 0, mà 1:0 không có nghĩa là "không lặp lần nào": dấu hai chấm đếm lùi:
Vòng lặp qua 1:length(empty) chạy hai lần, truy cập những phần tử không tồn tại (empty[1] là NA, còn phép gán thì tạo ra các phần tử ma). seq_along() trả về dãy rỗng cho vector rỗng, nên thân vòng lặp đơn giản là không bao giờ chạy. Hãy tập thói quen dùng seq_along(); hàm anh em seq_len(n) làm cùng việc khi bạn có một con số đếm thay vì một vector.
Gom kết quả: cấp phát trước, đừng nối dần
Phản xạ tự nhiên đầu tiên khi gom kết quả trông như sau:
squares <- c()
for (i in 1:10000) {
squares <- c(squares, i^2) # copies the ENTIRE vector every pass
}
Cách này chạy được, nhưng mỗi lần c(squares, ...) lại cấp phát một vector hoàn toàn mới và sao chép mọi phần tử cũ vào đó. Đến cuối, bạn đã sao chép khoảng 50 triệu con số chỉ để tạo ra 10.000 con số. Anti-pattern vector lớn dần này là lý do lớn nhất khiến người ta bảo vòng lặp R chậm.
Cách sửa: tạo kết quả với đủ kích thước ngay từ đầu, rồi gán theo chỉ số:
Với kết quả không phải số, cùng ý tưởng áp dụng với character(n), logical(n), hoặc vector("list", n) cho một list chứa đủ loại thứ. Vòng lặp được cấp phát trước chạy rất nhanh.
next và break
Hai từ khóa điều khiển vòng lặp từ bên trong thân của nó: next bỏ dở lượt hiện tại và nhảy sang phần tử kế tiếp; break thoát hẳn khỏi vòng lặp.
Đoạn này in ra 2 4 6 8: các số lẻ bị next bỏ qua, và khi đến 10, break kết thúc vòng lặp trước khi in. Dùng next để loại sớm các trường hợp (giúp thân chính không bị thụt lề thêm), và break khi vòng lặp đã tìm thấy thứ nó cần và không còn lý do để chạy tiếp.
Vòng lặp lồng nhau
Thân vòng lặp có thể chứa một vòng lặp khác. Vòng lặp trong chạy trọn vẹn cho mỗi lượt của vòng lặp ngoài; ví dụ kinh điển là bảng cửu chương:
Với mỗi hàng i, vòng lặp trong đi qua mọi cột j, và ký tự xuống dòng của hàng được in sau khi vòng lặp trong kết thúc. Lồng hai cấp thì ổn; từ ba cấp trở lên, thân vòng lặp thường đang muốn được tách thành một hàm, hoặc cả phép tính đang muốn trở thành outer() hay một phép toán ma trận.
Lồng nhau cũng dùng để lặp qua một list các vector: vòng ngoài lặp qua list, vòng trong (hoặc tốt hơn, một lệnh gọi vector hóa) xử lý từng phần tử:
Để ý rằng rốt cuộc không cần vòng lặp trong: mean() xử lý cả vector bên trong chỉ bằng một lệnh gọi. Nhận xét này áp dụng rộng hơn nhiều, và nó đưa ta đến phần thẳng thắn nhất.
Khi nào không nên dùng vòng lặp
R là ngôn ngữ vector hóa: các phép toán cơ bản của nó vốn đã làm việc trên cả vector cùng lúc. Một vòng lặp biến đổi từng phần tử một thường chỉ là cách viết dài hơn cho một dòng code:
Hãy ưu tiên dạng vector hóa mỗi khi có: ngắn hơn, khó sai hơn và nhanh hơn. Để áp dụng một hàm lên mọi phần tử của list, họ apply (sapply, lapply, vapply) đóng vai trò tương tự.
Nhưng đừng máy móc biến điều này thành "vòng lặp là xấu". Vòng lặp là công cụ đúng khi các lượt lặp phụ thuộc vào lượt trước (trạng thái tích lũy, mô phỏng), khi bạn tạo hiệu ứng phụ như ghi tệp, hoặc đơn giản khi vòng lặp là phiên bản mà bạn và người đọc hiểu ngay khi nhìn vào. Một vòng lặp rõ ràng tốt hơn một dòng code khéo léo mà không ai đọc nổi.
Những điều cần nhớ
for (x in v) { ... }chạy thân vòng lặp một lần cho mỗi phần tử, không cần quản lý bộ đếm.- Bên trong vòng lặp, in rõ ràng bằng
print()hoặccat(); biểu thức trơn sẽ bị bỏ đi. - Lặp theo vị trí bằng
seq_along(v), đừng bao giờ dùng1:length(v): cách sau chạy hai lần trên vector rỗng. - Cấp phát trước kết quả bằng
numeric(n)/vector("list", n)rồi gán theo chỉ số; nối dần bằngc()sẽ sao chép mọi thứ ở mỗi lượt. nextbỏ qua một lượt,breakthoát vòng lặp; ưu tiên phép toán vector hóa khi có, nhưng đừng ngại một vòng lặp dễ đọc.
Tiếp theo: những vòng lặp chạy cho đến khi có điều gì đó xảy ra thay vì một lần cho mỗi phần tử, tức while và repeat.
Câu hỏi thường gặp
Viết vòng lặp for trong R như thế nào?
for (x in v) { ... }: biến lặp x lần lượt nhận từng phần tử của vector (hoặc list) v, và thân vòng lặp chạy một lần cho mỗi phần tử. Muốn lặp theo vị trí thay vì theo giá trị, dùng for (i in seq_along(v)) rồi truy cập bằng v[i].
Vì sao nên dùng seq_along thay cho 1:length(v) trong R?
Khi v rỗng, length(v) bằng 0, nên 1:length(v) thành 1:0, tức vector hai phần tử c(1, 0), và thân vòng lặp chạy hai lần trên những phần tử không tồn tại. seq_along(v) trả về dãy rỗng cho vector rỗng, nên vòng lặp chạy đúng 0 lần.
Vì sao vòng lặp for trong R của mình không in ra gì?
R chỉ tự động in giá trị của biểu thức gõ ở cấp cao nhất của console. Bên trong thân vòng lặp, một biểu thức trơn như x được tính rồi bỏ đi. Hãy bọc nó trong print(x), hoặc dùng cat(...) khi bạn muốn tự định dạng đầu ra.
Vòng lặp for trong R có chậm không?
Bản thân vòng lặp không chậm. Cái chậm là làm vector lớn dần bên trong vòng lặp bằng result <- c(result, ...), vì mỗi lượt lại sao chép toàn bộ vector. Cấp phát trước bằng numeric(n) hoặc vector("list", n) rồi gán theo chỉ số, vòng lặp sẽ chạy rất tốt. Dù vậy, khi một phép toán vector hóa hay một lệnh gọi họ apply diễn đạt được cùng ý đó, cách ấy thường vừa nhanh hơn vừa dễ đọc hơn.