۱۱ سپتامبر ۲۰۲۳

کاری کردن

کاری کردن (Currying) یک شیوه پیشرفته کار با فانکشن ها است. این فقط مختص به جاوااسکریپت نیست و در زبان های دیگر نیز مورد استفاده قرار می‌گیرد.

کاری کردن به معنای تبدیل فانکشن ها به گونه‌ای است که فانکشنی که در ابتدا به صورت f(a, b, c) فراخوانی می‌شد، به شکل قابل فراخوانی f(a)(b)(c) تبدیل می‌شود.

به این نکته توجه کنید که کاری کردن فانکشن را صدا نمی‌زند بلکه فقط آن را تغییر می‌دهد.

برای درک بهتر این مفهوم، ابتدا یک مثال را مشاهده می‌کنیم و سپس کاربرد‌های عملی آن را بررسی می‌کنیم.

ما یک فانکشن کمکی با نام curry(f) ایجاد خواهیم کرد که برای دو آرگومان f کاری را اجرا می‌کند. به عبارتی دیگر، curry(f) برای دو آرگومان f(a, b) آن را به تابعی ترجمه می‌کند که به صورت f(a)(b) اجرا می‌شود.

function curry(f) { // تبدیل کاری را انجام می‌دهد curry(f)
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

// استفاده
function sum(a, b) {
  return a + b;
}

let curriedSum = curry(sum);

alert( curriedSum(1)(2) ); // 3

همانطور که می‌بینید، پیاده سازی بسیار ساده است: تنها دو بسته بندی صورت می‌گیرد.

  • نتیجه curry(func) یک بسته بندی به شکل function(a) است.
  • وقتی به صورت curriedSum(1) فراخوانی می‌شود، آرگومان در محیط واژگانی (the Lexical Environment) ذخیره می‌شود,و یک بسته بندی جدید به شکل function(b) بازگشت داده می‌شود .
  • سپس این بسته با 2 به عنوان آرگومان صدا زده می‌شود، و فراخوانی را به سمت فانکشن اصلی sum ارسال می‌کند.

پیاده سازی پیشرفته تری از کاری کردن، مثل _.curry از کتابخانه lodash، یک بسته بندی بر می‌گرداند که به کاربر این امکان را می‌دهد که به صورت عادی و یا جزئی فانکشن را فراخوانی کند.

function sum(a, b) {
  return a + b;
}

let curriedSum = _.curry(sum); // lodash استفاده کاری از کتابخانه

alert( curriedSum(1, 2) ); // 3, هنوز به شکل عادی قابل فراخوانی است
alert( curriedSum(1)(2) ); // 3, به شکل جزئی فراخوانده شده است

کاری کردن؟ چرا استفاده می‌شود؟

برای درک مزایای کاری کردن، به یک مثال واقعی و مفید نیاز داریم.

برای مثال، ما فانکشن log(date, importance, message) را داریم که اطلاعات را قالب بندی کرده و خروجی می‌دهد. در پروژه های واقعی چنین فانکشن هایی دارای بسیاری از ویژگی های مفید مانند ارسال لاگ ها از طریق شبکه هستند. در اینحا ما فقط از alert استفاده خواهیم کرد:

function log(date, importance, message) {
  alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}

بیایید کاری اش کنیم!

log = _.curry(log);

پس از آن log به شکل عادی کار می‌کند:

log(new Date(), "DEBUG", "some debug"); // log(a, b, c)

… و همینطور در شکل کاری شده کار می‌کند:

log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)

اکنون می توانیم به راحتی یک فانکشن ساده‌تر برای لاگ های کنونی ایجاد کنیم:

//  جزئی از لاگ با آرگومان اول ثابت خواهد بود
let logNow = log(new Date());

// استفاده از آن
logNow("INFO", "message"); // [HH:mm] INFO message

حالا log یک نسخه از فانکشن logNow است که آرگومان اول آن ثابت شده است. به عبارت دیگر “فانکشن جزئی اعمال شده” یا به اختصار “جزئی” است.

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

let debugNow = logNow("DEBUG");

debugNow("message"); // [HH:mm] DEBUG message

بنابراین:

  1. ما چیزی را بعد از کاری کردن از دست ندادیم: لاگ هنوز به شکل عادی قابل فراخوانی است.
  2. همچنین به راحتی می‌توانیم فانکشن‌های جزئی مانند لاگ‌های امروزی را ایجاد کنیم.

پیاده سازی پیشرفته کاری

در صورتی که مایلید وارد جزئیات شوید، اینجا پیاده سازی “پیشرفته” کاری برای فانکشن های دارای چند آرگومانه آمده است که می‌توانیم در بالا استفاده کنیم.

این پیاده سازی بسیار کوتاه است:

function curry(func) {

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

}

نمونه های استفاده:

function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

alert( curriedSum(1, 2, 3) ); // هنوز به شکل عادی قابل فراخوانی است، 6
alert( curriedSum(1)(2,3) ); // کاری کردن آرگومان اول، 6
alert( curriedSum(1)(2)(3) ); // کاری کردن کامل، 6

کاری جدید ممکن است به نظر پیچیده بیاید، اما در واقع درک آن آسان است.

نتیجه فراخوانی curry(func) بسته بندی curried است که به این شکل است:

// func فانکشن برای تبدیل است
function curried(...args) {
  if (args.length >= func.length) { // (1)
    return func.apply(this, args);
  } else {
    return function(...args2) { // (2)
      return curried.apply(this, args.concat(args2));
    }
  }
};

وقتی آن را اجرا می‌کنیم، دو شاخه اجرای شرطی if وجود دارد:

  1. اگر تعداد args ارسال شده یکسان یا بیشتر از فانکشن اصلی در تعریف آن باشد (func.length)، کافیست با استفاده از func.apply فراخوانی را به آن ارسال کنید.
  2. در غیر این صورت، یک فانکشن جزئی ایجاد می‌شود: ما فعلا func را صدا نمی‌زنیم. در عوض، یک بسته بندی دیگر بازگشت داده می‌شود، که مجدد کاری شدن را با فراهم کردن آرگومان های قبلی همراه با آرگومان های جدید اعمال می‌کند.

سپس، اگره دوباره آن را فراخوانی کنیم، دوباره، یا یک جزئی جدید(اگر آرگومان ها کافی نباشند) یا در نهایت نتیجه را دریافت می‌کنیم.

فقط فانکشن هایی با طول ثابت

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

فانکشنی که از پارامتر های rest استفاده می‌کند، مانند f(...args)، نمی‌تواند به این شکل کاری شود.

یکم بیشتر از کاری کردن

بر اساس تعریف، در کاری کردن sum(a, b, c) باید تبدل به sum(a)(b)(c) شود.

بیشتر پیاده سازی های کاری کردن در جاوااسکریپت پیشرفته هستند، همانطور که توضیح داده شد: آنها همچنین فانکشن را در نوع چند آرگومانی قابل فراخوانی نگه می‌دارند.

خلاصه

کاری کردن یک تبدیل کردن است که f(a,b,c) را به صورت f(a)(b)(c) قابل فراخوانی می‌کند. در پیاده سازی های جاوااسکریپت معمولا فانکشن را قابل فراخوانی به صورت عادی نگه می‌دارند و اگر تعداد آرگومان ها کافی نباشد، یک فانکشن جزئی را بر می‌گردانند.

کاری کردن به ما این امکان را می‌دهد که به راحتی فانکشن های جزئی را دریافت کنیم. همانطور که در مثال لاگ مشاهده کردیم، پس از اجرای سه آرگومان فانکشن یونیورسال log(date, importance, message) زمانی که با یک آرگومان فراخوانی می‌شود (مانند log(date)) یا دو آرگومان (مانند log(date, importance)) فانکشن جزئی را بر می‌گرداند.

نقشه آموزش