أساسيات JavaScript

الكاري والتطبيق الجزئي

45 دقيقة الدرس 49 من 60

ما هو الكاري؟

الكاري (Currying) هو تقنية أساسية في البرمجة الوظيفية تحول دالة ذات معاملات متعددة إلى سلسلة من الدوال، كل منها تقبل معاملا واحدا. سمي على اسم عالم الرياضيات هاسكل كاري، والكاري لا يستدعي الدالة -- بل يحولها. الدالة المكررة (curried) تأخذ معاملاتها واحدا تلو الآخر: تقدم المعامل الأول وتحصل على دالة جديدة تنتظر المعامل الثاني، وهكذا، حتى يتم تقديم جميع المعاملات ويتم إرجاع النتيجة النهائية. هذا المفهوم هو أحد أقوى الأنماط لبناء كود قابل لإعادة الاستخدام والتركيب في جافاسكريبت.

لفهم الكاري بشكل ملموس، خذ بعين الاعتبار دالة جمع بسيطة. عادة تكتب add(2, 3) وتحصل على 5. عند تطبيق الكاري، تكتب بدلا من ذلك add(2)(3). الاستدعاء الأول add(2) يرجع دالة جديدة تتذكر 2، وعندما تستدعى تلك الدالة المرجعة بـ 3، يتم إنتاج النتيجة النهائية 5. هذا التحويل يفتح عالما من الإمكانيات لإنشاء دوال متخصصة من دوال عامة.

الكاري اليدوي

أبسط طريقة لفهم الكاري هي كتابة دوال مكررة يدويا. بدلا من تعريف دالة تأخذ جميع معاملاتها دفعة واحدة، تقوم بتداخل الدوال بحيث تقبل كل واحدة معاملا واحدا بالضبط وترجع الدالة التالية في السلسلة.

مثال: الكاري اليدوي -- الجمع الأساسي

// النسخة غير المكررة
function addNormal(a, b) {
    return a + b;
}
console.log(addNormal(2, 3)); // 5

// النسخة المكررة يدويا
function addCurried(a) {
    return function(b) {
        return a + b;
    };
}

console.log(addCurried(2)(3)); // 5

// القوة: إنشاء دوال متخصصة
const addTen = addCurried(10);
console.log(addTen(5));  // 15
console.log(addTen(20)); // 30
console.log(addTen(3));  // 13

لاحظ كيف أن addCurried(10) ترجع دالة جديدة تماما تعرف بالفعل أن المعامل الأول هو 10. هذه الدالة المرجعة مخزنة في addTen ويمكن إعادة استخدامها عدة مرات حسب الحاجة. الدالة الداخلية لديها وصول إلى المعامل الخارجي a من خلال الإغلاق (closure) -- وهي الآلية التي تتذكر بها دوال جافاسكريبت المتغيرات من النطاق الذي أنشئت فيه.

مثال: الكاري اليدوي -- ثلاثة معاملات

// دالة مكررة بثلاثة معاملات
function multiply(a) {
    return function(b) {
        return function(c) {
            return a * b * c;
        };
    };
}

console.log(multiply(2)(3)(4)); // 24

// خطوة بخطوة
const double = multiply(2);       // ترجع function(b) {...}
const doubleThenTriple = double(3); // ترجع function(c) {...}
const result = doubleThenTriple(4); // 24

// إنشاء دوال متخصصة قابلة لإعادة الاستخدام
const tripleOf = multiply(1)(3);
console.log(tripleOf(10)); // 30
console.log(tripleOf(7));  // 21
نقطة أساسية: كل دالة مكررة بـ N معامل تصبح سلسلة من N دوال متداخلة، كل منها تأخذ معاملا واحدا بالضبط. الدالة الأعمق لديها وصول إلى جميع المعاملات الخارجية من خلال الإغلاقات. هذا ليس مجرد اختيار أسلوبي -- بل يمكن أنماط قوية من تركيب الدوال والتخصص التي تكون مستحيلة مع الدوال القياسية متعددة المعاملات.

الكاري مع دوال السهم

دوال السهم تجعل كود الكاري أكثر إيجازا بشكل كبير. لأن دالة السهم ذات المعامل الواحد لا تحتاج أقواسا حول معاملها ودالة السهم ذات التعبير الواحد لا تحتاج أقواسا معقوفة أو عبارة return، تصبح الدوال المكررة المتداخلة بعمق سطورا أنيقة واحدة.

مثال: الكاري بدوال السهم

// الكاري اليدوي بدوال السهم
const add = a => b => a + b;
const multiply = a => b => c => a * b * c;
const greet = greeting => name => `${greeting}, ${name}!`;

console.log(add(5)(3));           // 8
console.log(multiply(2)(3)(4));   // 24
console.log(greet('مرحبا')('أحمد')); // "مرحبا, أحمد!"

// إنشاء دوال متخصصة
const double = multiply(2)(1);
console.log(double(5));  // 10
console.log(double(12)); // 24

const sayHello = greet('مرحبا');
const sayGoodbye = greet('مع السلامة');
console.log(sayHello('سعيد'));    // "مرحبا, سعيد!"
console.log(sayGoodbye('سعيد')); // "مع السلامة, سعيد!"

قارن نسخة دالة السهم const add = a => b => a + b بالنسخة التقليدية بثلاثة مستويات من التداخل. صيغة السهم تقرأ تقريبا كتعريف رياضي، مما يجعل طبيعة الكاري للدالة واضحة فورا. هذا الإيجاز هو السبب في أن الكاري ودوال السهم رفيقان طبيعيان في جافاسكريبت الحديثة.

الكاري التلقائي -- بناء أداة كاري

كتابة الدوال المكررة يدويا تعمل جيدا للحالات البسيطة، لكن للدوال ذات المعاملات الكثيرة أو للدوال غير المكررة الموجودة، تحتاج أداة كاري تلقائية. دالة الكاري تأخذ أي دالة عادية وترجع نسخة مكررة منها. النقطة الأساسية هي أنك تفحص خاصية length للدالة (عدد المعاملات المتوقع) مقابل عدد المعاملات المقدمة حتى الآن. إذا تم تقديم معاملات كافية، استدعِ الدالة الأصلية. وإلا، أرجع دالة جديدة تجمع المزيد من المعاملات.

مثال: دالة كاري عامة

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

// الاستخدام مع دالة عادية
function volume(length, width, height) {
    return length * width * height;
}

const curriedVolume = curry(volume);

// كل هذه تعمل
console.log(curriedVolume(2)(3)(4));    // 24
console.log(curriedVolume(2, 3)(4));    // 24
console.log(curriedVolume(2)(3, 4));    // 24
console.log(curriedVolume(2, 3, 4));    // 24

أداة الكاري هذه مرنة -- تسمح لك بتقديم المعاملات واحدا تلو الآخر، أو عدة في وقت واحد، أو جميعها دفعة واحدة. تفحص ما إذا تم تجميع معاملات كافية بمقارنة args.length بـ fn.length. خاصية fn.length في جافاسكريبت ترجع عدد المعاملات المعرفة في توقيع الدالة. عندما تصل المعاملات المتراكمة أو تتجاوز ذلك العدد، تنفذ الدالة الأصلية بجميع المعاملات المجمعة.

مثال: نسخة دالة السهم من أداة الكاري

const curry = fn =>
    function curried(...args) {
        if (args.length >= fn.length) {
            return fn(...args);
        }
        return (...moreArgs) => curried(...args, ...moreArgs);
    };

// مثال: كاري لدالة تنسيق النصوص
const formatMessage = (level, timestamp, message) =>
    `[${level}] ${timestamp}: ${message}`;

const curriedFormat = curry(formatMessage);

const errorFormat = curriedFormat('ERROR');
const warningFormat = curriedFormat('WARNING');

const now = new Date().toISOString();
console.log(errorFormat(now)('فشل الاتصال بقاعدة البيانات'));
// "[ERROR] 2024-01-15T10:30:00.000Z: فشل الاتصال بقاعدة البيانات"

console.log(warningFormat(now)('استخدام الذاكرة فوق 80%'));
// "[WARNING] 2024-01-15T10:30:00.000Z: استخدام الذاكرة فوق 80%"
نصيحة احترافية: تعتمد أداة الكاري على fn.length لمعرفة عدد المعاملات المتوقعة. هذا يعني أنها لا تعمل بشكل صحيح مع معاملات rest (...args) أو المعاملات الافتراضية أو المعاملات المفككة، لأن هذه لا تحسب ضمن fn.length. على سبيل المثال، ((a, b = 1) => a + b).length هو 1 وليس 2. استخدم دائما معاملات صريحة بدون قيم افتراضية عند كتابة دوال مخصصة للكاري.

الكاري العملي -- معالجات الأحداث

الكاري مفيد بشكل خاص في البرمجة المدفوعة بالأحداث. عند إرفاق معالجات الأحداث، غالبا ما تحتاج إلى تمرير سياق إضافي مع كائن الحدث. يتيح لك الكاري إنشاء مصانع معالجات تنتج معالجات متخصصة لعناصر أو سيناريوهات مختلفة.

مثال: معالجات أحداث مكررة

// مصنع معالجات أحداث مكرر
const handleClick = action => elementId => event => {
    event.preventDefault();
    console.log(`الإجراء: ${action}، العنصر: ${elementId}`);
    console.log(`تم النقر عند: (${event.clientX}, ${event.clientY})`);
};

// إنشاء معالجات متخصصة
const handleDelete = handleClick('delete');
const handleEdit = handleClick('edit');

// إرفاق بعناصر محددة
const deleteBtn = document.getElementById('delete-user-42');
const editBtn = document.getElementById('edit-user-42');

if (deleteBtn) deleteBtn.addEventListener('click', handleDelete('user-42'));
if (editBtn) editBtn.addEventListener('click', handleEdit('user-42'));

// معالج مكرر للتحقق من صحة الإدخال
const validateInput = fieldName => minLength => event => {
    const value = event.target.value;
    if (value.length < minLength) {
        console.log(`${fieldName} يجب أن يكون ${minLength} أحرف على الأقل`);
        event.target.classList.add('invalid');
    } else {
        event.target.classList.remove('invalid');
    }
};

const validateUsername = validateInput('اسم المستخدم')(3);
const validatePassword = validateInput('كلمة المرور')(8);
const validateBio = validateInput('السيرة الذاتية')(20);

الكاري العملي -- طلبات API

بناء عملاء API هو مجال آخر يتألق فيه الكاري. يمكنك إنشاء دالة طلب مكررة تقبل أولا التكوين (عنوان URL الأساسي، الرؤوس)، ثم نقطة النهاية، ثم بيانات الطلب المحددة. كل مستوى من الكاري ينتج دالة أكثر تخصصا.

مثال: عميل API مكرر

// منشئ طلبات API مكرر
const createApiClient = baseUrl => defaultHeaders => method => endpoint => async data => {
    const config = {
        method,
        headers: {
            'Content-Type': 'application/json',
            ...defaultHeaders,
        },
    };

    if (data && method !== 'GET') {
        config.body = JSON.stringify(data);
    }

    const response = await fetch(`${baseUrl}${endpoint}`, config);
    return response.json();
};

// بناء دوال API متخصصة
const api = createApiClient('https://api.example.com');
const authApi = api({ 'Authorization': 'Bearer token123' });
const publicApi = api({});

// إنشاء دوال خاصة بالطريقة
const authGet = authApi('GET');
const authPost = authApi('POST');
const authPut = authApi('PUT');
const authDelete = authApi('DELETE');

// إنشاء دوال خاصة بنقطة النهاية
const getUsers = authGet('/users');
const getUser = id => authGet(`/users/${id}`);
const createUser = authPost('/users');
const updateUser = id => authPut(`/users/${id}`);

// الاستخدام -- نظيف ومعبر
const users = await getUsers();
const user = await getUser(42)();
const newUser = await createUser({ name: 'أحمد', email: 'ahmed@example.com' });
const updated = await updateUser(42)({ name: 'أحمد محدث' });
نقطة أساسية: لاحظ كيف أن كل مستوى من الكاري يضيف تحديدا. العميل الأساسي يعرف عنوان URL. إضافة الرؤوس تنشئ عميلا مصادقا أو عاما. إضافة الطريقة تنشئ عميل GET أو POST أو PUT أو DELETE. إضافة نقطة النهاية تنشئ دالة خاصة بالمورد. كل طبقة قابلة لإعادة الاستخدام بشكل مستقل. تبني العميل مرة واحدة وتستخدم النسخ المتخصصة في جميع أنحاء تطبيقك.

الكاري العملي -- التحقق من الصحة

التحقق من صحة النماذج هو مجال ينتج فيه الكاري كودا أنيقا وتصريحيا. بدلا من كتابة دوال تحقق متكررة، تنشئ مدققات عامة وتطبق عليها الكاري لتحويلها إلى قواعد محددة يمكن تركيبها معا.

مثال: مدققات مكررة

// مدققات عامة مكررة
const minLength = min => value =>
    value.length >= min ? null : `يجب أن يكون ${min} أحرف على الأقل`;

const maxLength = max => value =>
    value.length <= max ? null : `يجب ألا يتجاوز ${max} حرفا`;

const matches = regex => message => value =>
    regex.test(value) ? null : message;

const required = fieldName => value =>
    value && value.trim().length > 0 ? null : `${fieldName} مطلوب`;

const isEmail = matches(
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/
)('يرجى إدخال عنوان بريد إلكتروني صالح');

const isNumeric = matches(
    /^\d+$/
)('يجب أن يحتوي على أرقام فقط');

// تركيب المدققات في قواعد خاصة بالحقل
const validateField = validators => value => {
    for (const validate of validators) {
        const error = validate(value);
        if (error) return error;
    }
    return null;
};

// تعريف قواعد الحقول باستخدام مدققات مكررة
const usernameRules = validateField([
    required('اسم المستخدم'),
    minLength(3),
    maxLength(20),
    matches(/^[a-zA-Z0-9_]+$/)('اسم المستخدم يمكن أن يحتوي فقط على أحرف وأرقام وشرطات سفلية'),
]);

const emailRules = validateField([
    required('البريد الإلكتروني'),
    isEmail,
]);

const passwordRules = validateField([
    required('كلمة المرور'),
    minLength(8),
    matches(/[A-Z]/)('يجب أن يحتوي على حرف كبير واحد على الأقل'),
    matches(/[0-9]/)('يجب أن يحتوي على رقم واحد على الأقل'),
]);

// الاستخدام
console.log(usernameRules('ab'));           // "يجب أن يكون 3 أحرف على الأقل"
console.log(usernameRules('valid_user'));    // null (صالح)
console.log(emailRules('not-an-email'));     // "يرجى إدخال عنوان بريد إلكتروني صالح"
console.log(passwordRules('weak'));          // "يجب أن يكون 8 أحرف على الأقل"

التطبيق الجزئي مقابل الكاري

الكاري والتطبيق الجزئي مفهومان مرتبطان لكنهما مختلفان، والخلط بينهما هو أحد أكثر الأخطاء شيوعا في مناقشات البرمجة الوظيفية. الكاري يحول دالة ذات N معامل إلى N دوال كل منها بمعامل واحد. التطبيق الجزئي يثبت معاملا واحدا أو أكثر لدالة ويرجع دالة جديدة تأخذ المعاملات المتبقية -- لكن تلك الدالة المتبقية لا يزال بإمكانها قبول عدة معاملات دفعة واحدة.

مثال: الكاري مقابل التطبيق الجزئي

// الدالة الأصلية
function add(a, b, c) {
    return a + b + c;
}

// الكاري: تحويل إلى سلسلة من الدوال الأحادية
const curriedAdd = a => b => c => a + b + c;
curriedAdd(1)(2)(3); // 6
// كل استدعاء يأخذ معاملا واحدا بالضبط

// التطبيق الجزئي: تثبيت بعض المعاملات، إرجاع دالة للباقي
function partialAdd(a) {
    return function(b, c) {  // تقبل عدة معاملات متبقية
        return a + b + c;
    };
}
partialAdd(1)(2, 3); // 6
// الاستدعاء الأول يثبت معاملا واحدا، الثاني يقدم جميع المتبقية

// أداة تطبيق جزئي عامة
function partial(fn, ...fixedArgs) {
    return function(...remainingArgs) {
        return fn(...fixedArgs, ...remainingArgs);
    };
}

const addOne = partial(add, 1);
console.log(addOne(2, 3)); // 6  -- تقديم كلا المعاملين المتبقيين دفعة واحدة

const addOneTwo = partial(add, 1, 2);
console.log(addOneTwo(3)); // 6  -- معامل واحد متبقي فقط

الفرق الجوهري هو: الدالة المكررة ترجع دائما دالة أحادية (معامل واحد) حتى يتم تقديم جميع المعاملات. الدالة المطبقة جزئيا يمكن أن ترجع دالة تقبل معاملات متعددة. الكاري هو تحويل محدد لبنية الدالة؛ التطبيق الجزئي هو تقنية أكثر عمومية لتثبيت المعاملات. عمليا، أداة الكاري التلقائية التي بنيناها سابقا تطمس هذا الخط لأنها تسمح بتقديم معاملات متعددة دفعة واحدة، مما يجعلها تتصرف كالكاري والتطبيق الجزئي معا.

Function.prototype.bind للتطبيق الجزئي

جافاسكريبت لديها آلية مدمجة للتطبيق الجزئي: طريقة bind. بينما يستخدم bind بشكل أكثر شيوعا لتثبيت سياق this للدالة، فإنه يقبل أيضا معاملات إضافية تضاف في بداية قائمة المعاملات عند استدعاء الدالة المرتبطة.

مثال: التطبيق الجزئي باستخدام bind

function multiply(a, b) {
    return a * b;
}

// استخدام bind لإنشاء دوال مطبقة جزئيا
// null لسياق this لأننا لا نحتاجه
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);
const tenTimes = multiply.bind(null, 10);

console.log(double(5));   // 10
console.log(triple(5));   // 15
console.log(tenTimes(5)); // 50

// يعمل مع معاملات أكثر
function createTag(tag, className, content) {
    return `<${tag} class="${className}">${content}</${tag}>`;
}

const createDiv = createTag.bind(null, 'div');
const createHighlightDiv = createTag.bind(null, 'div', 'highlight');
const createParagraph = createTag.bind(null, 'p');

console.log(createDiv('container', 'مرحبا بالعالم'));
// "<div class=\"container\">مرحبا بالعالم</div>"

console.log(createHighlightDiv('نص مهم'));
// "<div class=\"highlight\">نص مهم</div>"

console.log(createParagraph('lead', 'الفقرة الأولى'));
// "<p class=\"lead\">الفقرة الأولى</p>"
مهم: عند استخدام bind للتطبيق الجزئي، المعامل الأول لـ bind يعين سياق this. إذا كنت تعمل مع دالة مستقلة (ليست طريقة)، مرر null كمعامل أول. ومع ذلك، إذا كنت تطبق جزئيا طريقة على كائن، مرر الكائن كمعامل أول للحفاظ على ربط this الصحيح. الخلط بين سياق this هو مصدر شائع للأخطاء عند استخدام bind للتطبيق الجزئي.

التطبيق الجزئي بالعناصر النائبة

التطبيق الجزئي القياسي وbind يثبتان المعاملات من اليسار إلى اليمين. لكن أحيانا تحتاج إلى تثبيت معاملات بترتيب مختلف. التطبيق الجزئي بالعناصر النائبة يتيح لك تخطي المواضع باستخدام قيمة حارسة (عنصر نائب) وملئها لاحقا. هذا مفيد بشكل خاص عندما لا يكون المعامل الذي تريد تثبيته هو الأول.

مثال: التطبيق الجزئي بالعناصر النائبة

// رمز العنصر النائب
const _ = Symbol('placeholder');

function partialWithPlaceholders(fn, ...partialArgs) {
    return function(...laterArgs) {
        const args = [];
        let laterIndex = 0;

        // ملء العناصر النائبة بالمعاملات اللاحقة
        for (const arg of partialArgs) {
            if (arg === _) {
                args.push(laterArgs[laterIndex++]);
            } else {
                args.push(arg);
            }
        }

        // إلحاق أي معاملات لاحقة متبقية
        while (laterIndex < laterArgs.length) {
            args.push(laterArgs[laterIndex++]);
        }

        return fn(...args);
    };
}

// مثال: تثبيت المعامل الثاني
function divide(a, b) {
    return a / b;
}

const divideBy2 = partialWithPlaceholders(divide, _, 2);
console.log(divideBy2(10)); // 5
console.log(divideBy2(20)); // 10

const half = partialWithPlaceholders(divide, _, 2);
const divideFrom100 = partialWithPlaceholders(divide, 100, _);

console.log(half(50));        // 25
console.log(divideFrom100(4)); // 25

// مفيد لطرق المصفوفات
const replace = partialWithPlaceholders(
    (str, search, replacement) => str.replace(search, replacement),
    _,
    /[aeiou]/gi,
    '*'
);

console.log(replace('Hello World')); // "H*ll* W*rld"

أسلوب خالي من النقاط

الأسلوب الخالي من النقاط (يسمى أيضا البرمجة الضمنية) هو طريقة لكتابة الدوال دون ذكر معاملاتها صراحة. بدلا من تعريف ما يحدث للبيانات، تركب العمليات معا. الكاري ضروري للأسلوب الخالي من النقاط لأنه ينتج دوالا جاهزة لقبول البيانات كمعاملها الأخير.

مثال: الأسلوب الخالي من النقاط مع الدوال المكررة

// دوال أدوات مكررة
const map = fn => arr => arr.map(fn);
const filter = predicate => arr => arr.filter(predicate);
const reduce = (fn, initial) => arr => arr.reduce(fn, initial);
const prop = key => obj => obj[key];
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

// خالي من النقاط: لا ذكر صريح للبيانات
const getNames = map(prop('name'));
const getActiveUsers = filter(prop('active'));
const sum = reduce((a, b) => a + b, 0);
const toUpperCase = str => str.toUpperCase();
const upperNames = map(toUpperCase);

// التركيب في خطوط أنابيب
const getActiveNames = pipe(
    getActiveUsers,
    getNames,
    upperNames
);

const users = [
    { name: 'أحمد', active: true },
    { name: 'سعيد', active: false },
    { name: 'محمد', active: true },
    { name: 'فاطمة', active: true },
];

console.log(getActiveNames(users));
// ["أحمد", "محمد", "فاطمة"]

// مقارنة بالنسخة الحتمية
const imperativeResult = users
    .filter(u => u.active)
    .map(u => u.name)
    .map(n => n.toUpperCase());
// نفس النتيجة لكن تذكر البيانات صراحة في كل خطوة
نصيحة احترافية: الأسلوب الخالي من النقاط يعمل بشكل أفضل عندما تتبع الدوال المكررة اتفاقية قبول معامل البيانات أخيرا. يسمى هذا أحيانا تصميم "البيانات أخيرا". مكتبات الأدوات مثل Ramda تتبع هذه الاتفاقية في جميع أنحائها، مما يجعل كل دالة قابلة للتركيب بشكل طبيعي بأسلوب خالٍ من النقاط. إذا صممت دوالك المكررة الخاصة، ضع دائما البيانات (الشيء الذي يتم تحويله) كمعامل أخير، وضع معاملات التكوين أو السلوك أولا.

الكاري للتكوين

أحد أكثر التطبيقات العملية للكاري هو بناء دوال قابلة للتكوين. معاملات التكوين تأتي أولا، والبيانات المراد معالجتها تأتي أخيرا. هذا ينشئ فصلا نظيفا بين الإعداد والتنفيذ، والدالة المكونة يمكن تخزينها وإعادة استخدامها عبر تطبيقك.

مثال: أنماط تكوين مكررة

// مسجل قابل للتكوين
const createLogger = level => prefix => message => {
    const timestamp = new Date().toISOString();
    const formatted = `[${timestamp}] [${level}] [${prefix}] ${message}`;

    if (level === 'ERROR') {
        console.error(formatted);
    } else if (level === 'WARNING') {
        console.warn(formatted);
    } else {
        console.log(formatted);
    }

    return formatted;
};

// إنشاء مسجلات متخصصة
const errorLog = createLogger('ERROR');
const warnLog = createLogger('WARNING');
const infoLog = createLogger('INFO');

// تخصيص أكثر حسب الوحدة
const dbError = errorLog('قاعدة البيانات');
const dbInfo = infoLog('قاعدة البيانات');
const authError = errorLog('المصادقة');
const authInfo = infoLog('المصادقة');
const apiInfo = infoLog('API');

// الاستخدام في جميع أنحاء التطبيق
dbError('انتهاء مهلة الاتصال بعد 30 ثانية');
dbInfo('تم تنفيذ الاستعلام في 45 مللي ثانية');
authError('رمز غير صالح للمستخدم 42');
authInfo('تم تسجيل دخول المستخدم 42 بنجاح');
apiInfo('GET /users استجاب بـ 200');

// منسق نصوص قابل للتكوين
const format = template => separator => values =>
    template.replace(/\{\}/g, () => values.shift() || separator);

const csvLine = format('{},{},{}')('');
const tsvLine = format('{}\t{}\t{}')('');

console.log(csvLine(['أحمد', '30', 'مهندس']));
// "أحمد,30,مهندس"

مثال واقعي: مدققات نماذج مكررة

دعنا نبني نظام تحقق من صحة النماذج كامل وبجودة إنتاجية باستخدام الكاري. كل مدقق هو دالة مكررة تقبل أولا تكوينها (رسائل الخطأ، القيود) ثم تقبل القيمة للتحقق منها. ترجع المدققات إما null للإدخال الصالح أو سلسلة خطأ للإدخال غير الصالح.

مثال: نظام تحقق مكرر كامل

// المدققات الأساسية -- كل منها مكرر للتكوين
const validators = {
    required: message => value =>
        value !== null && value !== undefined && String(value).trim() !== ''
            ? null
            : message || 'هذا الحقل مطلوب',

    minLength: (min, message) => value =>
        String(value).length >= min
            ? null
            : message || `الحد الأدنى ${min} أحرف مطلوبة`,

    maxLength: (max, message) => value =>
        String(value).length <= max
            ? null
            : message || `الحد الأقصى ${max} حرفا مسموح`,

    pattern: (regex, message) => value =>
        regex.test(String(value))
            ? null
            : message || 'تنسيق غير صالح',

    range: (min, max, message) => value => {
        const num = Number(value);
        return num >= min && num <= max
            ? null
            : message || `يجب أن يكون بين ${min} و ${max}`;
    },

    custom: (testFn, message) => value =>
        testFn(value) ? null : message,
};

// تركيب مدققات متعددة لحقل واحد
const composeValidators = (...rules) => value => {
    for (const rule of rules) {
        const error = rule(value);
        if (error) return error;
    }
    return null;
};

// تعريف مخطط النموذج الكامل
const formSchema = {
    username: composeValidators(
        validators.required('اسم المستخدم مطلوب'),
        validators.minLength(3, 'اسم المستخدم يجب أن يكون 3 أحرف على الأقل'),
        validators.maxLength(20, 'اسم المستخدم لا يمكن أن يتجاوز 20 حرفا'),
        validators.pattern(/^[a-zA-Z0-9_]+$/, 'أحرف وأرقام وشرطات سفلية فقط')
    ),
    email: composeValidators(
        validators.required('البريد الإلكتروني مطلوب'),
        validators.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'تنسيق بريد إلكتروني غير صالح')
    ),
    age: composeValidators(
        validators.required('العمر مطلوب'),
        validators.range(18, 120, 'يجب أن يكون بين 18 و 120')
    ),
    password: composeValidators(
        validators.required('كلمة المرور مطلوبة'),
        validators.minLength(8, 'كلمة المرور يجب أن تكون 8 أحرف على الأقل'),
        validators.pattern(/[A-Z]/, 'يجب أن تحتوي على حرف كبير'),
        validators.pattern(/[a-z]/, 'يجب أن تحتوي على حرف صغير'),
        validators.pattern(/[0-9]/, 'يجب أن تحتوي على رقم')
    ),
};

// التحقق من النموذج بالكامل
function validateForm(schema, data) {
    const errors = {};
    for (const [field, validate] of Object.entries(schema)) {
        const error = validate(data[field] || '');
        if (error) errors[field] = error;
    }
    return Object.keys(errors).length > 0 ? errors : null;
}

// اختبار
const formData = {
    username: 'ab',
    email: 'invalid',
    age: '15',
    password: 'weak',
};

console.log(validateForm(formSchema, formData));
// {
//   username: "اسم المستخدم يجب أن يكون 3 أحرف على الأقل",
//   email: "تنسيق بريد إلكتروني غير صالح",
//   age: "يجب أن يكون بين 18 و 120",
//   password: "كلمة المرور يجب أن تكون 8 أحرف على الأقل"
// }

مثال واقعي: عميل API مكرر مع معالجة الأخطاء

هنا عميل API متين يستخدم الكاري لإنشاء عميل HTTP طبقي قابل للتكوين. كل طبقة تضيف تحديدا: التكوين الأساسي، المصادقة، طريقة HTTP، نقطة النهاية، وأخيرا جسم الطلب. معالجة الأخطاء وتحليل الاستجابة مدمجة في الطبقات المكررة.

مثال: عميل API إنتاجي بالكاري

// منشئ عميل API مكرر مع معالجة الأخطاء
const createClient = baseConfig => authHeaders => method => endpoint => async (body = null) => {
    const url = `${baseConfig.baseUrl}${endpoint}`;

    const options = {
        method,
        headers: {
            'Content-Type': 'application/json',
            ...baseConfig.defaultHeaders,
            ...authHeaders,
        },
    };

    if (body && ['POST', 'PUT', 'PATCH'].includes(method)) {
        options.body = JSON.stringify(body);
    }

    try {
        const response = await fetch(url, options);

        if (!response.ok) {
            const errorData = await response.json().catch(() => ({}));
            throw {
                status: response.status,
                statusText: response.statusText,
                data: errorData,
                url,
            };
        }

        const contentType = response.headers.get('content-type');
        if (contentType && contentType.includes('application/json')) {
            return await response.json();
        }
        return await response.text();
    } catch (error) {
        if (error.status) throw error;
        throw { status: 0, statusText: 'خطأ في الشبكة', data: error.message, url };
    }
};

// تكوين العميل في طبقات
const config = { baseUrl: 'https://api.example.com/v2', defaultHeaders: {} };
const apiClient = createClient(config);

// عميل عام (بدون مصادقة)
const publicApi = apiClient({});
const publicGet = publicApi('GET');

// عميل مصادق
const authToken = 'eyJhbGciOiJIUzI1NiJ9...';
const authedApi = apiClient({ 'Authorization': `Bearer ${authToken}` });
const get = authedApi('GET');
const post = authedApi('POST');
const put = authedApi('PUT');
const del = authedApi('DELETE');

// دوال خاصة بالموارد
const getUsers = get('/users');
const getUserById = id => get(`/users/${id}`);
const createUser = post('/users');
const updateUser = id => put(`/users/${id}`);
const deleteUser = id => del(`/users/${id}`);

// الاستخدام
const allUsers = await getUsers();
const user = await getUserById(42)();
const newUser = await createUser({ name: 'أحمد', role: 'admin' });

متى تستخدم الكاري ومتى تتجنبه

الكاري قوي، لكنه ليس الأداة المناسبة لكل موقف. استخدم الكاري عندما تحتاج إلى إنشاء عائلات من الدوال المرتبطة من قالب عام، وعند بناء أدوات قابلة للتكوين، وعند العمل مع تركيب الدوال وخطوط الأنابيب، وعند تصميم مصانع معالجات الأحداث، أو عند تطبيق الأسلوب الخالي من النقاط. تجنب الكاري عندما تحتوي الدالة على عدد متغير من المعاملات، وعندما تكون جميع المعاملات متاحة دائما في نفس موقع الاستدعاء، وعندما يقلل من قابلية القراءة لفريقك، أو عندما يكون الأداء حرجا في حلقات ضيقة (كل استدعاء مكرر ينشئ كائن دالة وإغلاق جديدين).

مهم: الإفراط في استخدام الكاري يمكن أن يجعل الكود أصعب في القراءة، خاصة للمطورين غير المألوفين بأنماط البرمجة الوظيفية. الهدف هو الوضوح وليس الذكاء. استخدم الكاري عندما يبسط كودك حقا بإزالة التكرار أو تمكين التركيب. إذا كانت النسخة المكررة أصعب في الفهم من استدعاء دالة عادي، فضل النهج الأبسط. ضع دائما في اعتبارك مدى إلمام فريقك بهذه الأنماط قبل تبنيها على نطاق واسع في قاعدة كود مشتركة.

تمرين عملي

ابنِ خط أنابيب تحويل بيانات كامل باستخدام الكاري وتركيب الدوال. ابدأ بتنفيذ دالة أداة curry يمكنها تحويل أي دالة عادية إلى نسخة مكررة. ثم أنشئ الدوال المكررة التالية: map التي تأخذ دالة تحويل ثم مصفوفة، filter التي تأخذ شرطا ثم مصفوفة، sort التي تأخذ مقارنا ثم مصفوفة، take التي تأخذ عددا ثم مصفوفة، وprop التي تأخذ اسم مفتاح ثم كائنا. بعد ذلك، نفذ دالة pipe التي تركب الدوال من اليسار إلى اليمين. باستخدام هذه الأدوات، ابنِ خط أنابيب معالجة بيانات يأخذ مصفوفة من كائنات المنتجات (كل منها بخصائص name وprice وcategory وinStock) وينفذ التحويلات التالية في خط أنابيب مركب واحد: تصفية المنتجات المتوفرة فقط، تصفية المنتجات في فئة محددة (اجعل هذا قابلا للتكوين عبر الكاري)، الترتيب حسب السعر تصاعديا، أخذ أول 5 نتائج فقط، واستخراج الأسماء فقط. يجب أن يكون خط الأنابيب النهائي دالة واحدة تقبل الفئة كمعاملها الأول ومصفوفة المنتجات كمعاملها الثاني. اختبر خط أنابيبك بما لا يقل عن 10 منتجات عينة عبر 3 فئات، وتحقق من أن تغيير معامل الفئة ينتج نتائج مختلفة دون تعديل كود خط الأنابيب.