البرمجة الوظيفية هي نمط من تطوير البرنامج تستخدم فيه على نطاق واسع بعض الميزات المحددة للعمل مع الوظائف. يتعلق الأمر ، على وجه الخصوص ، بنقل الدالات إلى دالات أخرى كحجج وإرجاع دالات من دالات أخرى. ينتمي مفهوم "الوظائف النقية" أيضًا إلى النمط الوظيفي للبرمجة. يعتمد إخراج الوظائف المحضة فقط على المدخلات ، وعند تنفيذها ، لا تؤثر على حالة البرنامج.
مبادئ البرمجة الوظيفية مدعومة بالعديد من اللغات. من بينها JavaScript و Haskell و Clojure و Erlang. ينطوي استخدام آليات البرمجة الوظيفية على المعرفة ، من بين أمور أخرى ، بمفاهيم مثل الوظائف الخالصة ، ووظائف الكاري ، ووظائف الترتيب الأعلى.

المواد التي نترجمها اليوم هي عن الكاري. سنتحدث عن كيفية عمل الكاري وكيف يمكن أن تكون معرفة هذه الآلية مفيدة لمطور JS.
ما هو الكاري؟
Currying في البرمجة الوظيفية هو تحويل دالة مع العديد من الوسائط إلى مجموعة من الوظائف المتداخلة مع وسيطة واحدة. عندما يتم استدعاء دالة مع تمرير وسيطة واحدة إليها ، فإنها تُرجع دالة جديدة تتوقع وصول الوسيطة التالية. يتم إرجاع الدالات الجديدة التي تنتظر الوسيطة التالية في كل مرة يتم استدعاء الدالة curried - حتى تتلقى الدالة جميع الوسائط التي تحتاجها. الحجج التي تم تلقيها سابقًا ، بفضل آلية الإغلاق ، تنتظر اللحظة التي تحصل فيها الوظيفة على كل ما تحتاجه لإجراء الحسابات. بعد استلام الوسيطة الأخيرة ، تقوم الدالة بإجراء الحساب وإرجاع النتيجة.
بالحديث عن
الكاري ، يمكننا أن نقول أن هذه هي عملية تحويل دالة مع العديد من الحجج إلى وظيفة ذات قلة أقل.
Arity هو عدد الحجج للدالة. على سبيل المثال ، هنا هو الإعلان عن زوج من الوظائف:
function fn(a, b) { //... } function _fn(a, b, c) { //... }
تأخذ الدالة
fn
_fn
(هذه هي دالة ثنائية أو دالة 2-ary) ، تأخذ الدالة
_fn
ثلاث وسيطات (دالة ثلاثية ، 3-ary).
لنتحدث عن الموقف عندما يتم تحويل دالة بعدة وسيطات أثناء إجراء عملية التحليل إلى مجموعة من الوظائف ، كل منها يأخذ وسيطة واحدة.
تأمل في مثال. لدينا الوظيفة التالية:
function multiply(a, b, c) { return a * b * c; }
يستغرق ثلاث حجج ويعيد منتجهم:
multiply(1,2,3);
الآن دعونا نفكر في كيفية تحويلها إلى مجموعة من الوظائف ، كل منها يأخذ وسيطة واحدة. دعونا ننشئ نسخة كاري لهذه الوظيفة ونلقي نظرة على كيفية الحصول على نفس النتيجة عند استدعاء عدة وظائف:
function multiply(a) { return (b) => { return (c) => { return a * b * c } } } log(multiply(1)(2)(3))
كما ترى ، قمنا بتحويل المكالمة إلى دالة واحدة بثلاث وسيطات -
multiply(1,2,3)
في الاستدعاء لثلاث وظائف -
multiply(1)(2)(3)
.
اتضح أن وظيفة واحدة تحولت إلى عدة وظائف. عند استخدام البناء الجديد ، تقوم كل دالة ، باستثناء الوظيفة الأخيرة ، بإرجاع نتيجة الحسابات ، وتأخذ وسيطة وإرجاع دالة أخرى ، قادرة أيضًا على قبول وسيطة وإرجاع دالة أخرى. إذا كان إنشاء النموذج
multiply(1)(2)(3)
لا يبدو واضحًا لك ، دعنا نكتبه في هذا النموذج لفهم هذا بشكل أفضل:
const mul1 = multiply(1); const mul2 = mul1(2); const result = mul2(3); log(result);
الآن دعنا سطرا بسطر ما يحدث هنا.
أولاً ، نقوم بتمرير الوسيطة
1
إلى دالة
multiply
:
const mul1 = multiply(1);
عندما تعمل هذه الوظيفة ، يعمل هذا التصميم:
return (b) => { return (c) => { return a * b * c } }
الآن يحتوي
mul1
على مرجع إلى دالة تأخذ وسيطة
b
. نطلق على الدالة
mul1
تمريرها
2
:
const mul2 = mul1(2);
نتيجة لهذه المكالمة ، سيتم تنفيذ الرمز التالي:
return (c) => { return a * b * c }
mul2
على مرجع إلى دالة يمكن أن تكون فيه ، على سبيل المثال ، نتيجة للعملية التالية:
mul2 = (c) => { return a * b * c }
إذا قمنا الآن باستدعاء الدالة
mul2
، بتمريرها
3
، فستقوم الدالة بإجراء الحسابات اللازمة باستخدام الوسيطتين
a
و
b
:
const result = mul2(3);
ستكون نتيجة هذه الحسابات
6
:
log(result)
وظيفة
mul2
، التي تحتوي على أعلى مستوى من التعشيش ، يمكنها الوصول إلى النطاق ، إلى عمليات الإغلاق التي
mul1
و
mul1
. هذا هو السبب في
mul2
الممكن في الدالة
mul2
إجراء عمليات حسابية مع المتغيرات المعلنة في الدوال التي تم إكمال تنفيذها بالفعل ، والتي أعادت بالفعل بعض القيم وتمت معالجتها بواسطة جامع القمامة.
أعلاه درسنا مثالًا تجريديًا ، ولكن ، في جوهره ، نفس الوظيفة ، والتي تم تصميمها لحساب حجم صندوق مستطيل.
function volume(l,w,h) { return l * w * h; } const vol = volume(100,20,90)
إليك ما تبدو عليه نسخته الكاري:
function volume(l) { return (w) => { return (h) => { return l * w * h } } } const vol = volume(100)(20)(90)
لذلك ، يعتمد الكاري على الفكرة التالية: على أساس وظيفة معينة ، يتم إنشاء وظيفة أخرى ترجع وظيفة متخصصة.
تجفيف واستخدام جزئي للوظائف
الآن ، ربما ، هناك شعور بأن عدد الدوال المتداخلة ، عند تمثيل دالة كمجموعة من الدوال المتداخلة ، يعتمد على عدد الحجج للدالة. وإذا كان الأمر يتعلق بالكاري ، فهو كذلك.
يمكن إجراء نسخة خاصة من وظيفة حساب الحجم ، التي رأيناها بالفعل ، على النحو التالي:
function volume(l) { return (w, h) => { return l * w * h } }
هنا يتم تطبيق الأفكار ، مشابهة جدا لتلك التي نوقشت أعلاه. يمكنك استخدام هذه الوظيفة على النحو التالي:
const hV = volume(70); hV(203,142); hV(220,122); hV(120,123);
ويمكنك القيام بذلك:
volume(70)(90,30)
في الواقع ، هنا يمكنك أن ترى كيف ، من خلال الأمر
volume(70)
، أنشأنا وظيفة متخصصة لحساب حجم الأجسام ، يتم إصلاح أحد أبعادها (أي الطول ،
l
). تتوقع دالة
volume
3 وسيطات وتحتوي على دالتين متداخلتين ، على عكس الإصدار السابق من دالة مشابهة ، تحتوي النسخة الحالية منها على 3 وظائف متداخلة.
تطبق الوظيفة التي تم الحصول عليها بعد استدعاء
volume(70)
مفهوم تطبيق دالة جزئية. يتشابه التمارين والتطبيق الجزئي للوظائف مع بعضها البعض ، لكن المفاهيم مختلفة.
في التطبيق الجزئي ، يتم تحويل الدالة إلى دالة أخرى مع وسيطات أقل (أقل arity). يتم إصلاح بعض الحجج لهذه الدالة (يتم تعيين القيم الافتراضية لها).
على سبيل المثال ، هناك مثل هذه الوظيفة:
function acidityRatio(x, y, z) { return performOp(x,y,z) }
يمكن تحويلها إلى هذا:
function acidityRatio(x) { return (y,z) => { return performOp(x,y,z) } }
لم يتم تقديم تنفيذ
performOp()
هنا ، لأنه لا يؤثر على المفاهيم قيد النظر.
الوظيفة التي يمكن الحصول عليها عن طريق استدعاء الدالة
acidityRatio()
الجديدة بحجة تحتاج إلى إصلاح قيمتها هي الوظيفة الأصلية ، والتي يتم إصلاح إحدى الوسيطات فيها ، وهذه الوظيفة نفسها تأخذ وسيطة واحدة أقل من الأصل.
ستبدو النسخة المنشقة للدالة كما يلي:
function acidityRatio(x) { return (y) = > { return (z) = > { return performOp(x,y,z) } } }
كما ترى ، عند التسوية ، فإن عدد الوظائف المتداخلة يساوي عدد وسيطات الدالة الأصلية. كل من هذه الوظائف تتوقع حجتها الخاصة. من الواضح أنه إذا لم تقبل وظيفة الوسيطات ، أو قبلت وسيطة واحدة فقط ، فلا يمكن التفكير فيها.
في الحالة التي تحتوي فيها الدالة على حجتين ، يمكن القول أن نتائج تجعيدها وتطبيقها الجزئي تتزامن. على سبيل المثال ، لدينا مثل هذه الوظيفة:
function div(x,y) { return x/y; }
لنفترض أننا بحاجة إلى إعادة كتابتها حتى نتمكن من إصلاح الوسيطة الأولى من الحصول على دالة تقوم بإجراء العمليات الحسابية عند تمرير الوسيطة الثانية فقط إليها ، أي أننا نحتاج إلى تطبيق هذه الوظيفة جزئيًا. سيبدو هذا:
function div(x) { return (y) => { return x/y; } }
ستبدو نتيجة الكاري بنفس الطريقة تمامًا.
حول التطبيق العملي لمفاهيم الكاري والتطبيق الجزئي للوظائف
يمكن أن يكون التجفيف والتطبيق الجزئي للوظائف مفيدًا في مواقف مختلفة. على سبيل المثال ، عند تطوير وحدات صغيرة مناسبة لإعادة الاستخدام.
يسهّل الاستخدام الجزئي للوظائف استخدام الوحدات النمطية العامة. على سبيل المثال ، لدينا متجر على الإنترنت ، يوجد في رمزه وظيفة يتم استخدامها لحساب المبلغ المستحق مع مراعاة الخصم.
function discount(price, discount) { return price * discount }
هناك فئة معينة من العملاء ، دعنا نسميهم "العملاء المحبوبون" ، الذين نقدم لهم خصمًا بنسبة 10٪. على سبيل المثال ، إذا اشترى مثل هذا العميل شيئًا مقابل 500 دولار ، فإننا نمنحه خصمًا قدره 50 دولارًا:
const price = discount(500,0.10); // $50 // $500 - $50 = $450
من السهل ملاحظة أنه باستخدام هذا النهج ، يتعين علينا باستمرار استدعاء هذه الوظيفة بحجتين:
const price = discount(1500,0.10); // $150 // $1,500 - $150 = $1,350 const price = discount(2000,0.10); // $200 // $2,000 - $200 = $1,800 const price = discount(50,0.10); // $5 // $50 - $5 = $45 const price = discount(5000,0.10); // $500 // $5,000 - $500 = $4,500 const price = discount(300,0.10); // $30 // $300 - $30 = $270
يمكن اختزال الوظيفة الأصلية إلى نموذج يسمح لك بتلقي وظائف جديدة بمستوى خصم محدد مسبقًا ، عند الاستدعاء وهو ما يكفي لتحويل مبلغ الشراء. تحتوي الدالة
discount()
في مثالنا على وسيطتين. إليك ما يبدو عليه حيث قمنا بتحويله:
function discount(discount) { return (price) => { return price * discount; } } const tenPercentDiscount = discount(0.1);
tenPercentDiscount()
هي نتيجة التطبيق الجزئي للدالة
discount()
. عند استدعاء
tenPercentDiscount()
هذه الوظيفة ، يكفي تمرير السعر ، وسيتم تحديد خصم 10٪ ، أي وسيطة
discount
، بالفعل:
tenPercentDiscount(500); // $50 // $500 - $50 = $450
إذا كان هناك مشترين في متجرنا قرروا منح خصم 20 ٪ ، فيمكنك الحصول على الوظيفة المناسبة للعمل معهم على النحو التالي:
const twentyPercentDiscount = discount(0.2);
يمكن الآن
twentyPercentDiscount()
الدالة
twentyPercentDiscount()
لحساب تكلفة البضائع ، مع الأخذ بعين الاعتبار خصم 20٪:
twentyPercentDiscount(500); // 100 // $500 - $100 = $400 twentyPercentDiscount(5000); // 1000 // $5,000 - $1,000 = $4,000 twentyPercentDiscount(1000000); // 200000 // $1,000,000 - $200,000 = $600,000
الوظيفة العامة للتطبيق الجزئي للوظائف الأخرى
سنقوم بتطوير دالة تقبل أي وظيفة وتعيد متغيرها ، وهي دالة تم تعيين بعض الحجج لها بالفعل. إليك الشفرة التي تسمح لك بالقيام بذلك (أنت ، إذا شرعت في تطوير وظيفة مماثلة ، فمن المحتمل جدًا أن تحصل على شيء آخر نتيجة لذلك):
function partial(fn, ...args) { return (..._arg) => { return fn(...args, ..._arg); } }
تقبل الدالة
partial()
الدالة
fn
، التي نريد تحويلها إلى الدالة المطبقة جزئيًا ، وعدد متغير من المعلمات
(...args
). تُستخدم العبارة
rest
لوضع جميع المعلمات بعد
fn
في
args
.
تُرجع هذه الدالة دالة أخرى تقبل أيضًا عددًا متغيرًا من المعلمات (
_arg
). هذه الوظيفة ، بدورها ، تستدعي وظيفة
fn
الأصلية ، وتمررها المعلمات
...args
و
...args
..._arg
(باستخدام عامل
spread
). تقوم الوظيفة بإجراء الحساب وإرجاع النتيجة.
نستخدم هذه الوظيفة لإنشاء متغير من دالة
volume
المألوفة لك بالفعل ، المصممة لحساب حجم المتوازيات المستطيلة ، التي تم إصلاح أحد جوانبها:
function volume(l,h,w) { return l * h * w } const hV = partial(volume,100); hV(200,900); // 18000000 hV(70,60); // 420000
هنا يمكنك أن تجد مثالا على وظيفة عالمية لتجميع الوظائف الأخرى.
الملخص
في هذه المقالة ، تحدثنا عن الكاري والتطبيق الجزئي للوظائف. يتم تنفيذ هذه الأساليب لتحويل الدوال في JavaScript بسبب عمليات الإغلاق وبسبب حقيقة أن الدوال في JS هي كائنات من الفئة الأولى (يمكن تمريرها كحجج إلى وظائف أخرى ، يتم إرجاعها منها ، يتم تعيينها للمتغيرات).
أعزائي القراء! هل تستخدم تقنيات الكاري والتطبيق الجزئي للوظائف في مشاريعك؟
