DEV Community

Function — Higher Order Function

Higher-order function: tái sử dụng logic qua hàm nhận và trả về hàm

Higher-order function (HOF) là hàm nhận hàm khác làm đối số, hoặc trả về một hàm. map, filter, reduce là HOF; mọi middleware, mọi decorator, mọi wrapper retry/cache đều là HOF. Chúng là công cụ chính để tách cái cần làm (logic nghiệp vụ) khỏi khung điều khiển (lặp, thử lại, đo thời gian), nhờ đó cùng một khung dùng lại cho nhiều logic. Sức mạnh đó đi kèm một cái bẫy thực tế: lạm dụng HOF tạo ra các tầng trừu tượng khiến stack trace khó đọc và bug khó lần.

Cơ chế hoạt động

HOF hoạt động được nhờ hàm trong JS là first-class value: gán cho biến, truyền làm đối số, trả về từ hàm khác như mọi giá trị khác. Một HOF trả về hàm gần như luôn dựa vào closure để "nhớ" cấu hình.

function withRetry(fn, { times = 3, delayMs = 200 } = {}) {
  return async function (...args) {
    let lastErr
    for (let i = 0; i < times; i++) {
      try {
        return await fn(...args)
      } catch (err) {
        lastErr = err
        await new Promise((r) => setTimeout(r, delayMs * 2 ** i)) // backoff
      }
    }
    throw lastErr
  }
}

const fetchUser = withRetry(rawFetchUser, { times: 5 })
Enter fullscreen mode Exit fullscreen mode

withRetry không biết gì về rawFetchUser làm gì — nó chỉ bọc một khung retry với exponential backoff quanh bất kỳ hàm async nào. Logic gọi API và logic thử lại tách rời, nên khung retry này dùng được cho mọi lệnh gọi mạng trong hệ thống.

Vấn đề gặp trong production

Tách concern dùng lại được. Các mối quan tâm xuyên suốt — retry, timeout, cache, đo thời gian, log, rate limit — đều hợp với HOF vì chúng là khung bao quanh logic, không phụ thuộc logic cụ thể:

const withTimeout = (fn, ms) => async (...args) =>
  Promise.race([fn(...args), new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))])

const safeFetch = withTimeout(withRetry(rawFetchUser), 3000) // ghép nhiều lớp
Enter fullscreen mode Exit fullscreen mode

Ghép HOF cho phép xây hành vi phức tạp từ các mảnh nhỏ độc lập. map/filter/reduce là dạng quen thuộc nhất — xử lý collection mà không viết vòng lặp thủ công, ít lỗi off-by-one và biến tạm.

Failure mode: over-abstraction. Đây là rủi ro thật của HOF. Lồng quá nhiều lớp wrapper làm hai thứ tệ đi. Một, stack trace ngập các frame ẩn danh của wrapper, không chỉ ra dòng nghiệp vụ thật sự fail:

at <anonymous> (retry.js:5)
at <anonymous> (timeout.js:3)
at <anonymous> (cache.js:8)   // logic thật bị chôn dưới ba lớp
Enter fullscreen mode Exit fullscreen mode

Hai, luồng dữ liệu trở nên khó lần: muốn hiểu một giá trị đi qua đâu phải nhảy qua nhiều hàm trả-về-hàm. Một chuỗi reduce lồng map lồng filter "thông minh" thường khó đọc hơn một vòng lặp thẳng. Dấu hiệu lạm dụng: phải đọc ba bốn hàm mới hiểu một thao tác đơn giản, hoặc đặt tên hàm wrapper kiểu withXWithYWithZ.

Cách giảm đau: đặt tên hàm tường minh thay vì arrow ẩn danh (để tên hiện trong stack trace), giới hạn độ sâu ghép wrapper, và dùng vòng lặp thường khi logic tuần tự đơn giản — không phải mọi thứ đều phải là reduce.

Cách debug và monitor

Khi stack trace toàn <anonymous>, đổi các hàm trả về của HOF từ arrow ẩn danh sang named function expression — tên sẽ hiện trong trace và chỉ thẳng vào lớp wrapper đang lỗi. Khi một pipeline map/filter/reduce cho kết quả sai, tách nó ra từng bước và log trung gian thay vì đọc cả chuỗi một lúc; phần lớn bug nằm ở một bước (quên return trong reduce, predicate filter ngược điều kiện). Đo: với HOF thêm chi phí (Promise, closure mỗi lần gọi), nếu nằm trong hot path thì benchmark — đôi khi một vòng lặp thẳng nhanh hơn đáng kể.

Tradeoff

HOF tăng tái sử dụng và tách concern: viết khung retry/timeout/cache một lần, dùng khắp nơi, và map/filter/reduce diễn đạt ý định gọn hơn vòng lặp. Cái giá là tầng trừu tượng — debug khó hơn (stack trace gãy, luồng dữ liệu gián tiếp) và đôi khi tốn hiệu năng hơn vòng lặp thuần. Quy tắc thực tế: dùng HOF cho concern xuyên suốt thật sự lặp lại nhiều chỗ; với logic dùng một lần hoặc tuần tự đơn giản, code thẳng dễ đọc và dễ sửa hơn. Trừu tượng chỉ đáng khi nó che giấu sự phức tạp lặp lại, không phải khi nó thêm một lớp cho một lần dùng.

Câu hỏi phỏng vấn

HOF là gì, dùng để giải quyết vấn đề gì, và đánh đổi khi lạm dụng?

HOF là hàm nhận hàm làm đối số hoặc trả về hàm, làm được nhờ hàm là first-class value và thường dựa vào closure để giữ cấu hình. Nó giải quyết bài toán tách logic nghiệp vụ khỏi khung điều khiển — viết một khung retry/timeout/cache/log dùng chung cho mọi logic, và map/filter/reduce thay vòng lặp thủ công. Đánh đổi khi lạm dụng: mỗi lớp wrapper thêm một frame ẩn danh làm stack trace khó đọc, luồng dữ liệu trở nên gián tiếp khó lần, và có chi phí hiệu năng. Điểm ăn điểm là nêu cách kiểm soát: đặt tên hàm để hiện trong trace, giới hạn độ sâu ghép, và dùng vòng lặp thẳng cho logic tuần tự đơn giản.

Hands-on

Viết một bộ HOF dùng được trong production: withRetry (exponential backoff), withTimeout, và withCache (TTL), rồi ghép chúng quanh một lệnh gọi API thật và kiểm tra hành vi khi API chậm/lỗi. Cố tình để các hàm trả về là arrow ẩn danh, gây một lỗi bên trong, và đọc stack trace để thấy nó vô dụng; sau đó đổi sang named function expression và xác nhận trace chỉ đúng lớp lỗi. Cuối cùng viết lại một pipeline filter→map→reduce phức tạp bằng một vòng lặp thẳng và so sánh độ dễ đọc lẫn hiệu năng trên một dataset lớn.

Top comments (0)