الإغلاقات والكاري
ما هي الإغلاقات؟
الإغلاق هو دالة تلتقط المتغيرات من النطاق المعجمي المحيط بها. حتى بعد أن تُرجع الدالة الخارجية قيمتها، يحتفظ الإغلاق بالوصول إلى تلك المتغيرات. في Dart، كل دالة هي إغلاق — يمكنها “الإغلاق على” المتغيرات المعرّفة في نطاقها المحيط.
مثال إغلاق أساسي
void main() {
String greeting = 'Hello';
void sayHello(String name) {
// هذه الدالة الداخلية "تلتقط" greeting من النطاق الخارجي
print('$greeting, $name!');
}
sayHello('Edrees'); // Hello, Edrees!
greeting = 'Hi';
sayHello('Edrees'); // Hi, Edrees! (تلتقط المتغير وليس القيمة)
}
النطاق المعجمي في Dart
يستخدم Dart النطاق المعجمي، ويُسمى أيضاً النطاق الثابت. يمكن للدالة الوصول إلى المتغيرات من كل نطاق محيط، من متغيراتها المحلية إلى الخارج حتى النطاق الأعلى. يتحدد النطاق بهيكل الكود وقت الكتابة وليس وقت التشغيل.
سلسلة النطاقات المتداخلة
String topLevel = 'I am top-level';
void outerFunction() {
String outerVar = 'I am outer';
void middleFunction() {
String middleVar = 'I am middle';
void innerFunction() {
String innerVar = 'I am inner';
// innerFunction يمكنها الوصول لجميع المتغيرات:
print(topLevel); // OK
print(outerVar); // OK
print(middleVar); // OK
print(innerVar); // OK
}
innerFunction();
// print(innerVar); // خطأ: innerVar ليست في النطاق هنا
}
middleFunction();
}
void main() {
outerFunction();
}
إرجاع الدوال (مصانع الإغلاقات)
أحد أقوى الأنماط مع الإغلاقات هو إرجاع دالة من دالة أخرى. الدالة المُرجعة تتذكر المتغيرات من سياق إنشائها، حتى بعد انتهاء تنفيذ الدالة الخارجية.
مصنع العدّاد
Function makeCounter({int start = 0, int step = 1}) {
int count = start;
return () {
count += step;
return count;
};
}
void main() {
final counter1 = makeCounter();
print(counter1()); // 1
print(counter1()); // 2
print(counter1()); // 3
final counter2 = makeCounter(start: 10, step: 5);
print(counter2()); // 15
print(counter2()); // 20
// counter1 و counter2 لهما حالة مستقلة!
print(counter1()); // 4
}
makeCounter() ينشئ متغير count جديد تماماً. الإغلاق المُرجع يلتقط ذلك المتغير المحدد، لذا العدّادات المتعددة مستقلة تماماً عن بعضها البعض. هذه طريقة نظيفة لإنشاء سلوك ذي حالة بدون فئات.الإغلاقات على متغيرات الحلقات
مشكلة شائعة في كثير من اللغات هي التقاط متغيرات الحلقة داخل الإغلاقات. في Dart، استخدام for-in أو حلقة for القياسية ينشئ متغيراً جديداً لكل تكرار، مما يتجنب الخلل الكلاسيكي.
الإغلاقات في الحلقات — السلوك الصحيح
void main() {
final functions = <Function>[];
// كل تكرار ينشئ 'i' جديد، لذا كل إغلاق يلتقط نسخته الخاصة
for (var i = 0; i < 5; i++) {
functions.add(() => print(i));
}
for (var fn in functions) {
fn(); // يطبع 0, 1, 2, 3, 4 (كل إغلاق له i الخاص به)
}
}
مشكلة محتملة مع المتغيرات المشتركة
void main() {
final functions = <Function>[];
var shared = 0;
for (var i = 0; i < 5; i++) {
functions.add(() => print(shared));
shared++;
}
// جميع الإغلاقات تشترك في نفس المتغير 'shared'
for (var fn in functions) {
fn(); // يطبع 5, 5, 5, 5, 5 (الكل يرى القيمة النهائية لـ shared)
}
}
التطبيق الجزئي
التطبيق الجزئي هو تقنية تثبّت فيها بعض وسائط الدالة، مما ينتج دالة جديدة تأخذ الوسائط المتبقية. هذا مفيد للغاية لإنشاء نسخ متخصصة من الدوال العامة.
التطبيق الجزئي في الممارسة
// دالة عامة تأخذ ثلاث وسائط
double calculatePrice(double basePrice, double taxRate, double discount) {
return basePrice * (1 + taxRate) - discount;
}
// التطبيق الجزئي: تثبيت معدل الضريبة لمنطقة محددة
Function applyTax(double taxRate) {
return (double basePrice, double discount) {
return calculatePrice(basePrice, taxRate, discount);
};
}
void main() {
// إنشاء دوال تسعير خاصة بالمنطقة
final usPricing = applyTax(0.08); // ضريبة 8%
final euPricing = applyTax(0.20); // ضريبة القيمة المضافة 20%
print(usPricing(100.0, 10.0)); // 100 * 1.08 - 10 = 98.0
print(euPricing(100.0, 10.0)); // 100 * 1.20 - 10 = 110.0
}
الكاري (Currying)
الكاري يحوّل دالة تأخذ وسائط متعددة إلى سلسلة من الدوال، كل منها تأخذ وسيطاً واحداً. بينما التطبيق الجزئي يثبّت بعض الوسائط دفعة واحدة، الكاري يحوّل بشكل صارم إلى دوال ذات وسيط واحد.
تحويل دالة إلى كاري
// دالة عادية متعددة الوسائط
int add(int a, int b) => a + b;
// النسخة المحوّلة بالكاري: تُرجع سلسلة من دوال ذات وسيط واحد
int Function(int) Function(int) curriedAdd = (int a) => (int b) => a + b;
void main() {
// استخدام الدالة المحوّلة بالكاري خطوة بخطوة
final addFive = curriedAdd(5);
print(addFive(3)); // 8
print(addFive(10)); // 15
// أو استدعاؤها دفعة واحدة
print(curriedAdd(2)(3)); // 5
}
مساعد كاري عام
// دالة كاري عامة لدوال ذات وسيطين
C Function(B) Function(A) curry2<A, B, C>(C Function(A, B) fn) {
return (A a) => (B b) => fn(a, b);
}
// دالة كاري عامة لدوال ذات ثلاث وسائط
D Function(C) Function(B) Function(A) curry3<A, B, C, D>(
D Function(A, B, C) fn) {
return (A a) => (B b) => (C c) => fn(a, b, c);
}
String formatGreeting(String greeting, String title, String name) {
return '$greeting, $title $name!';
}
void main() {
final curriedGreet = curry3(formatGreeting);
final helloTo = curriedGreet('Hello');
final helloMr = helloTo('Mr.');
final helloDr = helloTo('Dr.');
print(helloMr('Smith')); // Hello, Mr. Smith!
print(helloDr('Jones')); // Hello, Dr. Jones!
}
مثال عملي: التخزين المؤقت (Memoization)
التخزين المؤقت يستخدم الإغلاقات لتخزين نتائج استدعاءات الدوال المكلفة. عند الاستدعاء مرة أخرى بنفس الوسائط، تُرجع النتيجة المخزّنة بدلاً من إعادة الحساب.
التخزين المؤقت بالإغلاقات
/// ينشئ نسخة مُخزنة مؤقتاً من دالة ذات وسيط واحد.
R Function(T) memoize<T, R>(R Function(T) fn) {
final cache = <T, R>{};
return (T arg) {
if (cache.containsKey(arg)) {
print(' Cache hit for $arg');
return cache[arg] as R;
}
print(' Computing for $arg');
final result = fn(arg);
cache[arg] = result;
return result;
};
}
int expensiveSquare(int n) {
// محاكاة حساب مكلف
return n * n;
}
void main() {
final memoSquare = memoize(expensiveSquare);
print(memoSquare(5)); // Computing for 5 → 25
print(memoSquare(5)); // Cache hit for 5 → 25
print(memoSquare(10)); // Computing for 10 → 100
print(memoSquare(10)); // Cache hit for 10 → 100
}
مثال عملي: مصانع معالجات الأحداث
الإغلاقات مثالية لإنشاء معالجات أحداث متخصصة. بدلاً من تمرير بيانات الإعدادات في كل مكان، تُدمجها في الإغلاق وقت الإنشاء.
مصنع معالجات الأحداث
typedef EventHandler = void Function(String eventData);
EventHandler createLogger(String component, {bool verbose = false}) {
int eventCount = 0;
return (String eventData) {
eventCount++;
if (verbose) {
print('[$component] Event #$eventCount: $eventData '
'(timestamp: ${DateTime.now()})');
} else {
print('[$component] $eventData');
}
};
}
void main() {
final authLogger = createLogger('Auth', verbose: true);
final dbLogger = createLogger('Database');
authLogger('User logged in');
// [Auth] Event #1: User logged in (timestamp: ...)
authLogger('Token refreshed');
// [Auth] Event #2: Token refreshed (timestamp: ...)
dbLogger('Query executed');
// [Database] Query executed
}
مثال عملي: بُناة الإعدادات
تمكّن الإغلاقات نمط البنّاء حيث تُرجع كل خطوة دالة جديدة، تتراكم الإعدادات تدريجياً.
بنّاء الإعدادات بالإغلاقات
typedef Validator = bool Function(String);
Validator createValidator({
int? minLength,
int? maxLength,
RegExp? pattern,
List<String>? blacklist,
}) {
return (String input) {
if (minLength != null && input.length < minLength) return false;
if (maxLength != null && input.length > maxLength) return false;
if (pattern != null && !pattern.hasMatch(input)) return false;
if (blacklist != null && blacklist.contains(input)) return false;
return true;
};
}
/// تجمع عدة مُحققات في واحد يتطلب نجاح الجميع.
Validator composeValidators(List<Validator> validators) {
return (String input) => validators.every((v) => v(input));
}
void main() {
final usernameValidator = composeValidators([
createValidator(minLength: 3, maxLength: 20),
createValidator(pattern: RegExp(r'^[a-zA-Z0-9_]+$')),
createValidator(blacklist: ['admin', 'root', 'system']),
]);
print(usernameValidator('edrees_95')); // true
print(usernameValidator('ab')); // false (قصير جداً)
print(usernameValidator('admin')); // false (في القائمة السوداء)
print(usernameValidator('hello world')); // false (يحتوي مسافة)
}
الإغلاقات مقابل الدوال المجهولة مقابل الدوال المسمّاة
من المهم فهم العلاقة بين هذه المفاهيم:
مقارنة أنواع الدوال
void main() {
int multiplier = 3;
// دالة مسمّاة (أيضاً إغلاق إذا التقطت متغيرات)
int tripleNamed(int x) => x * multiplier;
// دالة مجهولة (لامدا) وهي أيضاً إغلاق
final tripleAnon = (int x) => x * multiplier;
// اختصار دالة السهم (أيضاً إغلاق)
final tripleArrow = (int x) => x * multiplier;
// الثلاثة تنتج نفس النتيجة:
print(tripleNamed(5)); // 15
print(tripleAnon(5)); // 15
print(tripleArrow(5)); // 15
// المفتاح: كل هذه إغلاقات لأنها تلتقط 'multiplier'
multiplier = 10;
print(tripleNamed(5)); // 50 (ترى multiplier المحدّث)
}
مثال عملي: فيبوناتشي مع التخزين المؤقت
الجمع بين الإغلاقات والتخزين المؤقت لتحسين متتالية فيبوناتشي الكلاسيكية يوضح قوة الإغلاقات في العالم الحقيقي.
فيبوناتشي مع التخزين المؤقت
/// ينشئ دالة فيبوناتشي مُخزنة مؤقتاً باستخدام الإغلاقات.
int Function(int) createFibonacci() {
final cache = <int, int>{};
int fib(int n) {
if (cache.containsKey(n)) return cache[n]!;
if (n <= 1) return n;
final result = fib(n - 1) + fib(n - 2);
cache[n] = result;
return result;
}
return fib;
}
void main() {
final fibonacci = createFibonacci();
// فعّال حتى للأرقام الكبيرة بفضل التخزين المؤقت
for (var i = 0; i <= 10; i++) {
print('fib($i) = ${fibonacci(i)}');
}
// fib(0) = 0, fib(1) = 1, fib(2) = 1, ..., fib(10) = 55
// هذا سيكون بطيئاً بشكل مستحيل بدون التخزين المؤقت
print('fib(40) = ${fibonacci(40)}'); // 102334155 (فوري!)
}
الملخص
الإغلاقات هي حجر الأساس للبرمجة الوظيفية في Dart. تُمكّن أنماطاً قوية مثل دوال المصنع والتخزين المؤقت والتطبيق الجزئي والكاري. النقاط الرئيسية:
- الإغلاقات تلتقط المتغيرات (وليس القيم) من نطاقها المعجمي.
- حلقة
forفي Dart تنشئ متغيراً جديداً لكل تكرار، متجنبة خلل الإغلاق الكلاسيكي على متغير الحلقة. - التطبيق الجزئي يثبّت بعض وسائط الدالة؛ الكاري يحوّل إلى سلسلة دوال ذات وسيط واحد.
- التخزين المؤقت يستخدم خريطة مُخزنة مؤقتاً ملتقطة بالإغلاق لحفظ النتائج المحسوبة سابقاً.
- مصانع الإغلاقات تنشئ نسخاً مستقلة بحالتها الخاصة، مشابهة للكائنات لكن أخف.