1. Currying đổi cách gọi hàm ra sao

Currying là kỹ thuật biến đổi một hàm nhận nhiều tham số thành một chuỗi các hàm, mỗi hàm chỉ nhận một tham số. Tên gọi này được đặt theo nhà toán học Haskell Curry.

// Hàm thông thường
const add = (a, b) => a + b;

// Hàm curry
const curriedAdd = (a) => (b) => a + b;

// Sử dụng
console.log(add(2, 3)); // 5
console.log(curriedAdd(2)(3)); // 5

2. Partial application dùng để cố định tham số nào

Partial Application là kỹ thuật cố định một số tham số của hàm, tạo ra một hàm mới với ít tham số hơn.

// Hàm gốc
const multiply = (a, b, c) => a * b * c;

// Partial application
const multiplyByTwo = multiply.bind(null, 2);
// hoặc
const multiplyByTwo2 = (b, c) => multiply(2, b, c);

console.log(multiplyByTwo(3, 4)); // 24 (2 * 3 * 4)

3. Triển khai trong JavaScript

3.1 Curry Helper Function

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return function (...args2) {
      return curried.apply(this, args.concat(args2));
    };
  };
}

// Sử dụng
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);

console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6

3.2 Partial Application Helper

function partial(fn, ...args) {
  return function (...restArgs) {
    return fn.apply(this, [...args, ...restArgs]);
  };
}

// Sử dụng
const greet = (greeting, name) => `${greeting}, ${name}!`;
const sayHello = partial(greet, "Hello");

console.log(sayHello("John")); // "Hello, John!"

4. Triển khai trong TypeScript

4.1 Type-safe Currying

type Curry<P extends any[], R> = P extends [infer First, ...infer Rest]
  ? (arg: First) => Rest extends [] ? R : Curry<Rest, R>
  : R;

function curry<P extends any[], R>(fn: (...args: P) => R): Curry<P, R> {
  return function curried(...args: any[]): any {
    if (args.length >= fn.length) {
      return fn(...args);
    }
    return function (...args2: any[]) {
      return curried(...args.concat(args2));
    };
  } as any;
}

// Sử dụng với type safety
const add = (a: number, b: number, c: number): number => a + b + c;
const curriedAdd = curry(add);

// TypeScript sẽ bảo đảm type safety
const result = curriedAdd(1)(2)(3); // type: number

4.2 Type-safe Partial Application

type Partial<T, P extends any[]> = (...args: P) => T;

function partial<T, P extends any[], R extends any[]>(
  fn: (...args: [...P, ...R]) => T,
  ...partialArgs: P
): Partial<T, R> {
  return (...restArgs: R) => fn(...partialArgs, ...restArgs);
}

// Sử dụng với type safety
interface User {
  id: number;
  name: string;
}

const createUser = (
  role: string,
  id: number,
  name: string
): User & { role: string } => ({
  id,
  name,
  role,
});

const createAdmin = partial(createUser, "admin");
const admin = createAdmin(1, "John"); // type: User & { role: string }

5. Dùng trong thực tế: cố định cấu hình API

interface ApiConfig {
  baseUrl: string;
  headers: Record<string, string>;
}

interface ApiResponse<T> {
  data: T;
  status: number;
}

// Curried fetch function
const createApiClient =
  (config: ApiConfig) =>
  (method: string) =>
  (endpoint: string) =>
  async <T>(body?: any): Promise<ApiResponse<T>> => {
    const response = await fetch(`${config.baseUrl}${endpoint}`, {
      method,
      headers: config.headers,
      body: body ? JSON.stringify(body) : undefined,
    });

    const data = await response.json();
    return {
      data,
      status: response.status,
    };
  };

// Sử dụng
const config: ApiConfig = {
  baseUrl: "https://api.example.com",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer token",
  },
};

const api = createApiClient(config);
const get = api("GET");
const post = api("POST");

// Endpoints
const getUser = get("/users");
const createUser = post("/users");

// Sử dụng
interface User {
  id: number;
  name: string;
}

async function example() {
  const user = await getUser<User>();
  const newUser = await createUser<User>({ name: "John" });
}

Điểm hay của partial application trong case này là mình cố định baseUrl, headers, rồi method, rồi endpoint. Nhưng nếu chain quá dài, stack trace và autocomplete sẽ kém thân thiện. API client production thường nên dừng ở mức này:

const withAuth =
  (token: string) =>
  (init: RequestInit = {}): RequestInit => ({
    ...init,
    headers: {
      ...init.headers,
      Authorization: `Bearer ${token}`,
    },
  });

const withJson = (init: RequestInit = {}): RequestInit => ({
  ...init,
  headers: {
    ...init.headers,
    "Content-Type": "application/json",
  },
});

const buildRequest = (token: string): RequestInit =>
  withJson(withAuth(token)());

Ở đây partial application chỉ giữ một việc: cấu hình request theo từng lớp. Nếu bạn phải đọc từ phải sang trái qua 5-6 hàm mới hiểu request thật sự là gì, pattern đã đi quá xa.

6. Test và debug currying

Currying dễ tạo bug khi số lượng tham số (fn.length) không phản ánh đúng hàm thật, nhất là với default params, rest params, hoặc overload TypeScript. Vì vậy helper curry() generic nên được dùng hạn chế; với business logic quan trọng, viết hàm chuyên biệt thường rõ hơn.

const createMoneyFormatter =
  (currency: string) =>
  (locale: string) =>
  (amount: number): string =>
    new Intl.NumberFormat(locale, { style: "currency", currency }).format(
      amount
    );

const formatVnd = createMoneyFormatter("VND")("vi-VN");

console.assert(formatVnd(120000).includes("120"));

Test nên gọi vào hàm đã partial thật sự (formatVnd), không chỉ test hàm gốc. Bug thường nằm ở lớp cấu hình được cố định sai, chứ không nằm ở phép xử lý cuối cùng.

7. Khi nào không nên dùng

  • Khi team không quen đọc hàm trả về hàm.
  • Khi function có nhiều tham số optional hoặc default value.
  • Khi stack trace quan trọng hơn sự gọn gàng của composition.
  • Khi một object config rõ ràng hơn nhiều tham số được curry từng lớp.

Nếu thấy phải giải thích “hàm này gọi ba lần mới chạy”, có lẽ object config sẽ thân thiện hơn.

8. Trade-off cần nhớ

8.1 Ưu điểm

  • Tái sử dụng: Tạo ra các hàm chuyên biệt từ hàm tổng quát
  • Linh hoạt: Dễ dàng tạo ra các biến thể của hàm
  • Type safety: TypeScript hỗ trợ tốt cho cả hai kỹ thuật
  • Composition: Dễ dàng kết hợp với Function Composition

8.2 Nhược điểm

  • Phức tạp: Có thể khó hiểu với người mới
  • Debug: Khó debug khi có nhiều lớp currying
  • Performance: Overhead do tạo nhiều closure
  • Readability: Syntax có thể khó đọc với nhiều dấu ngoặc

9. Khi nào nên dùng

Currying và Partial Application phù hợp khi:

  • Cần tạo ra các hàm chuyên biệt từ hàm tổng quát
  • Muốn tái sử dụng logic với các tham số khác nhau
  • Làm việc với Function Composition
  • Xây dựng API linh hoạt

Điểm đáng giữ là cố định đúng phần thay đổi chậm, ví dụ config, locale, currency, logger, feature flag. Đừng curry mọi thứ chỉ vì có thể.