مفهوم Currying در جاوااسکریپت فرآیندی در برنامه نویسی فانکشنال است که در آن می‌توانیم یک تابع با چندین آرگومان را به دنباله‌ای از توابع تودرتو تبدیل کنیم. به این ترتیب آن تابع، یک تابع جدیدی را return می‌کند که منتظر آرگومان بعدی می‌باشد.

به عبارت دیگر، یک تابع به جای اینکه همه آرگومان‌ها را در یک زمان دریافت کند، آرگومان اول را می‌گیرد و تابعی را برمی‌گرداند که آرگومان دوم را دریافت می‌کند، سپس تابع جدیدی را برمی‌گرداند که آرگومان سوم را می‌گیرد و به همین ترتیب ادامه پیدا می‌کند تا زمانی که همه آرگومان‌ها دریافت شوند. مثلا وقتی که تابع sum(1,2,3)را به sum(1)(2)(3)تبدیل می‌کنیم.

به تعداد آرگومان‌هایی که یک تابع می‌گیرد arityنیز گفته می‌شود.

function sum(a, b) {
    // do something
}
function _sum(a, b, c) {
    // do something
}

تابع sumدو آرگومان (تابع ۲-arity) و _sumسه آرگومان (تابع ۳-arity) دریافت می‌کند.

توابع Curried با زنجیره‌ای از closureها و با تعریف و return هم‌زمان توابع داخلی آن‌ها ساخته می‌شوند.

چرا Currying در برنامه نویسی مفید است؟

Currying به ما کمک می‌کند تا از استفاده مجدد و دوباره یک متغیر مشابه جلوگیری کنیم. این مفهوم همچنین به ایجاد یک تابع higher order کمک می‌کند. ما در مقاله توابع Higher Order در جاوااسکریپت این توابع را به شکل مورد بررسی قرار داده‌ایم.

Currying یک تابع با چندین آرگومان را به یک دنباله از توابع تبدیل می‌کند که هر کدام یک آرگومان واحد می‌گیرند. به عنوان مثال:

function sum(a, b, c) {
    return a + b + c;
}
sum(1,2,3); // 6

همانطور که می بینیم، در این مثال تابع هر سه آرگومان را باهم و در یک زمان دریافت می‌کند. در ادامه ورژنی از تابع را ایجاد می‌کنیم تا ببینیم چگونه می‌توانیم یک تابع را در یک سری فراخوانی، فراخوانی ‌کرده و همان نتیجه را بدست بیاوریم:

function sum(a) {
    return (b) => {
        return (c) => {
            return a + b + c
        }
    }
}
console.log(sum(1)(2)(3)) // 6

برای درک بهتر می‌توانیم این مجموع (۱) (۲) (۳) را از هم جدا کنیم:

const sum1 = sum(1);
const sum2 = sum1(2);
const result = sum2(3);
console.log(result); // 6

در ادامه نحوه کارکردن آن را بررسی می‌کنیم.

ما عدد ۱ را به تابع sumارسال کردیم:

let sum1 = sum(1);

تابع زیر را برمی‌گرداند:

return (b) => {
        return (c) => {
            return a + b + c
        }
}

اکنون sum1تعریف تابع بالا را دارد که آرگومان bرا می‌گیرد.

ما تابع sum1 را با تخصیص دادن مقدار ۲ فراخوانی می‌کنیم:

let sum2 = sum1(2);

sum1 تابع سوم را برمی‌گرداند:

return (c) => {
            return a + b + c
}

حالا تابع return شده در متغیر sum2ذخیره می‌شود.

sum2 اکنون به صورت زیر خواهد بود:

sum2 = (c) => {
            return a + b + c
}

وقتی sum2 با پارامتر ۳ فراخوانی شود:

const result = sum2(3);

محاسبه را با پارامترهای قبلی انجام می‌دهد: a = 1،b = 2و مقدار ۶ را برمی‌گرداند.

console.log(result); // 6

آخرین تابع فقط متغیرcرا می‌پذیرد اما این عملیات را با متغیرهای دیگری که Scope تابع آن‌ها return شده است انجام می‌دهد. با این وجود به دلیل Closure درست کار می‌کند.

Currying و Partial Application

اکنون ممکن است برخی به این موضوع فکر کنند که تعداد توابع تودرتو یک تابع Curried به تعداد آرگومان‌هایی که دریافت می‌کند بستگی دارد. بله، این موضوع آن را تبدیل به Curry می‌کند.

همان مثال sumرا در نظر می‌گیریم:

function sum(a) {
    return (b, c) => {
        return a * b * c
    }
}

می‌تواند به شکل زیر فراخوانی شود:

let x = sum(10);
x(3,12);
x(20,12);
x(20,13);
// OR
sum(10)(3,12);
sum(10)(20,12);
sum(10)(20,13);

تابع فوق انتظار دارد ۳ آرگومان دریافت کند و ۲ تابع تودرتو داشته باشد، برخلاف ورژن قبلی که انتظار دریافت ۳ آرگومان وجود داشت و ۳ تابع تودرتو ایجاد شد.

این ورژن Curry نیست. ما فقط یک Partial Application از تابع sum انجام دادیم.

Currying و Partial Application به دلیل closure با هم مرتبط هستند، اما مفاهیم متفاوتی دارند.

Partial Application یک تابع را به تابع دیگری با arity کوچک‌تر تبدیل می‌کند.

function sum1(x, y, z) {
    return sum2(x,y,z)
}
// to
function sum1(x) {
    return (y,z) => {
        return sum2(x,y,z)
    }
}

این مثال برای Currying به صورت زیر خواهد بود:

function sum1(x) {
    return (y) = > {
        return (z) = > {
            return sum2(x,y,z)
        }
    }
}

Currying با توجه به تعداد آرگومان‌های تابع، توابع تودرتو ایجاد می‌کند. هر تابع یک آرگومان دریافت می‌کند. اگر آرگومانی وجود نداشته باشد، Curryingای نیز وجود نخواهد داشت.

برای توسعه تابعی که یک تابع را می‌گیرد و تابع Curried برمی‌گرداند به صورت زیر عمل می‌کنیم:

function currying(fn, ...args) {
    return (..._arg) => {
        return fn(...args, ..._arg);
    }
}

تابع بالا یک تابع (fn)که می‌خواهیم آن را اجرا کنیم و تعداد متغیر پارامترها (…args)را دریافت می‌کند. عملگر rest برای جمع‌آوری تعداد پارامترها بعد از fnدر …argsمورد استفاده قرار می‌گیرد.

در مرحله بعد، تابعی را return می‌کنیم که بقیه پارامترها را نیز به صورت …_argsجمع‌آوری می‌کند. این تابع، تابع اصلی fn را با استفاده از عملگر spread به عنوان پارامتر به …argsو …_argsارسال می‌کند، سپس مقدار به کاربر return می‌شود.

اکنون می‌توانیم از تابع بالا برای ایجاد تابع curry استفاده کنیم:

function sum(a,b,c) {
    return a + b + c
}
let add = currying(sum,10);
add(20,90); // 120
add(70,60); // 140

Closure مفهوم Currying را در جاوااسکریپت امکان‌پذیر می‌کند. اگر با مفهوم Scope و closure آشنا نیستید مطالعه مقاله Scope و Closure در جاوااسکریپت می‌تواند بسیار مفید باشد.

جمع‌بندی

در این مقاله سعی کردیم با مفهوم Currying در جاوااسکریپت آشنا شویم و نحوه عملکرد آن را دقیق‌تر یاد بگیریم.