البرمجة الوظيفية: الدوال عالية الرتبة
الدوال ككائنات من الدرجة الأولى
في Dart، الدوال هي كائنات من الدرجة الأولى. هذا يعني أن الدوال يمكن إسنادها لمتغيرات، وتمريرها كمعاملات لدوال أخرى، وإرجاعها من دوال، وتخزينها في هياكل بيانات -- تماماً مثل أي قيمة أخرى. هذا هو أساس البرمجة الوظيفية في Dart.
الدوال هي كائنات
void main() {
// إسناد دالة لمتغير
final greet = (String name) => 'Hello, $name!';
print(greet('Edrees')); // Hello, Edrees!
// تخزين الدوال في قائمة
final operations = <int Function(int, int)>[
(a, b) => a + b,
(a, b) => a - b,
(a, b) => a * b,
];
for (final op in operations) {
print(op(10, 3)); // 13, 7, 30
}
// تخزين الدوال في خريطة
final validators = <String, bool Function(String)>{
'email': (s) => s.contains('@'),
'phone': (s) => s.length >= 10,
'name': (s) => s.isNotEmpty,
};
print(validators['email']!('test@mail.com')); // true
print(validators['phone']!('12345')); // false
// التحقق من نوع الدالة
print(greet.runtimeType); // (String) => String
print(greet is Function); // true
}
تمرير الدوال كمعاملات
الدالة عالية الرتبة هي دالة تأخذ دالة واحدة أو أكثر كمعاملات، أو تُرجع دالة. هذا واحد من أقوى الأنماط في Dart، يمكّنك من كتابة كود قابل لإعادة الاستخدام والتركيب بدرجة عالية.
الدوال كمعاملات
// دالة عالية الرتبة تأخذ دالة مقارنة
List<T> customSort<T>(List<T> items, int Function(T, T) compare) {
final sorted = List<T>.from(items);
sorted.sort(compare);
return sorted;
}
// دالة عالية الرتبة تأخذ شرطاً
List<T> filterItems<T>(List<T> items, bool Function(T) predicate) {
return items.where(predicate).toList();
}
// دالة عالية الرتبة تأخذ محولاً
List<R> transformItems<T, R>(List<T> items, R Function(T) transform) {
return items.map(transform).toList();
}
void main() {
final names = ['Edrees', 'Ahmed', 'Sara', 'Zain', 'Layla'];
// الترتيب حسب الطول
final byLength = customSort(names, (a, b) => a.length.compareTo(b.length));
print(byLength); // [Sara, Zain, Ahmed, Layla, Edrees]
// تصفية الأسماء بأكثر من 4 أحرف
final longNames = filterItems(names, (n) => n.length > 4);
print(longNames); // [Edrees, Ahmed, Layla]
// التحويل إلى أحرف كبيرة
final upper = transformItems(names, (n) => n.toUpperCase());
print(upper); // [EDREES, AHMED, SARA, ZAIN, LAYLA]
}
إرجاع الدوال
الدوال يمكنها أيضاً إرجاع دوال أخرى. هذا يتيح أنماطاً قوية مثل مصانع الدوال، والكاري، والإغلاقات التي "تتذكر" سياق إنشائها.
دوال تُرجع دوالاً
// مصنع دوال: ينشئ مدققات متخصصة
bool Function(String) createLengthValidator(int minLength, int maxLength) {
return (String value) => value.length >= minLength && value.length <= maxLength;
}
// مصنع دوال: ينشئ عمليات رياضية
double Function(double) createMultiplier(double factor) {
return (double value) => value * factor;
}
// إغلاق: الدالة المُرجعة "تتذكر" العداد
int Function() createCounter([int start = 0]) {
int count = start;
return () => count++;
}
// الكاري: تحويل دالة متعددة المعاملات إلى سلسلة
String Function(String) Function(String) greetWith(String greeting) {
return (String title) {
return (String name) => '$greeting, $title $name!';
};
}
void main() {
// إنشاء مدققات متخصصة
final usernameValidator = createLengthValidator(3, 20);
final passwordValidator = createLengthValidator(8, 128);
print(usernameValidator('Ed')); // false (قصير جداً)
print(usernameValidator('Edrees')); // true
print(passwordValidator('12345')); // false (قصير جداً)
print(passwordValidator('secure_password_123')); // true
// إنشاء مضاعفات
final double2x = createMultiplier(2);
final taxRate = createMultiplier(1.15);
print(double2x(50)); // 100.0
print(taxRate(100)); // 115.0
// العداد يوضح الإغلاق
final counter = createCounter(10);
print(counter()); // 10
print(counter()); // 11
print(counter()); // 12
// الكاري
final helloMr = greetWith('Hello')('Mr.');
print(helloMr('Ahmed')); // Hello, Mr. Ahmed!
print(helloMr('Zain')); // Hello, Mr. Zain!
}
createCounter، الدالة المُرجعة تلتقط متغير count و"تتذكره" بين الاستدعاءات. كل استدعاء لـ createCounter ينشئ إغلاقاً جديداً مستقلاً بمتغير count خاص به.map() و where() و reduce()
هذه هي الطرق الثلاث الأساسية عالية الرتبة على مجموعات Dart. تشكل العمود الفقري لمعالجة البيانات بأسلوب وظيفي.
map() -- تحويل كل عنصر
void main() {
final numbers = [1, 2, 3, 4, 5];
// map() تطبق دالة على كل عنصر وتُرجع Iterable جديد
final doubled = numbers.map((n) => n * 2);
print(doubled.toList()); // [2, 4, 6, 8, 10]
final words = ['hello', 'world', 'dart'];
final capitalized = words.map((w) => w[0].toUpperCase() + w.substring(1));
print(capitalized.toList()); // [Hello, World, Dart]
// map() مع الفهرس باستخدام .indexed (Dart 3)
final indexed = words.indexed.map((pair) => '${pair.$1}: ${pair.$2}');
print(indexed.toList()); // [0: hello, 1: world, 2: dart]
// تحويل النوع: map() يمكنها تغيير نوع العنصر
final lengths = words.map((w) => w.length);
print(lengths.toList()); // [5, 5, 4]
// lengths هو Iterable<int>، وليس Iterable<String>
}
map() تُرجع Iterable كسولاً، وليس List. دالة التحويل تُستدعى فقط عند استهلاك المُكرر (التكرار عليه، تحويله لقائمة، إلخ). هذا يعني أنه إذا كان للتحويل آثار جانبية، فلن تحدث حتى يتم الوصول للمُكرر. استدعِ دائماً .toList() إذا كنت بحاجة لتقييم فوري أو تحتاج استخدام النتيجة كـ List.where() -- تصفية العناصر
void main() {
final numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// where() تحتفظ فقط بالعناصر التي تجتاز الاختبار
final evens = numbers.where((n) => n.isEven);
print(evens.toList()); // [2, 4, 6, 8, 10]
final greaterThan5 = numbers.where((n) => n > 5);
print(greaterThan5.toList()); // [6, 7, 8, 9, 10]
// السلسلة مع map
final evenSquares = numbers
.where((n) => n.isEven)
.map((n) => n * n);
print(evenSquares.toList()); // [4, 16, 36, 64, 100]
// whereType<T>() -- تصفية حسب النوع
final mixed = [1, 'hello', 3.14, true, 42, 'world'];
final strings = mixed.whereType<String>();
print(strings.toList()); // [hello, world]
final ints = mixed.whereType<int>();
print(ints.toList()); // [1, 42]
}
reduce() و fold() -- التجميع إلى قيمة واحدة
void main() {
final numbers = [1, 2, 3, 4, 5];
// reduce() تجمع كل العناصر باستخدام دالة
// تبدأ بالعنصر الأول كمجمّع ابتدائي
final sum = numbers.reduce((acc, n) => acc + n);
print(sum); // 15
final product = numbers.reduce((acc, n) => acc * n);
print(product); // 120
final maxVal = numbers.reduce((a, b) => a > b ? a : b);
print(maxVal); // 5
// fold() مثل reduce() لكن تأخذ قيمة ابتدائية صريحة
// fold() يمكنها أيضاً تغيير نوع الإرجاع
final sumWithStart = numbers.fold(100, (acc, n) => acc + n);
print(sumWithStart); // 115
// fold() لربط نصوص من أعداد صحيحة
final csv = numbers.fold('', (acc, n) => acc.isEmpty ? '$n' : '$acc,$n');
print(csv); // 1,2,3,4,5
// fold() لبناء خريطة
final words = ['apple', 'banana', 'avocado', 'blueberry', 'apricot'];
final grouped = words.fold<Map<String, List<String>>>(
{},
(map, word) {
final key = word[0]; // الحرف الأول
(map[key] ??= []).add(word);
return map;
},
);
print(grouped);
// {a: [apple, avocado, apricot], b: [banana, blueberry]}
}
fold() بدلاً من reduce() عندما: (1) المجموعة قد تكون فارغة (reduce() ترمي على المجموعات الفارغة)، (2) تحتاج قيمة ابتدائية محددة، أو (3) نوع النتيجة يختلف عن نوع العنصر. reduce() أبسط عندما تعرف أن المجموعة غير فارغة ونوع النتيجة يطابق نوع العنصر.expand() و every() و any()
هذه الطرق عالية الرتبة تكمل map() و where() و reduce() لعمليات المجموعات الشائعة.
expand() و every() و any()
void main() {
// expand() -- flatMap: حول كل عنصر إلى مُكرر، ثم افرده
final sentences = ['Hello world', 'Dart is great'];
final allWords = sentences.expand((s) => s.split(' '));
print(allWords.toList()); // [Hello, world, Dart, is, great]
// توسيع القوائم المتداخلة
final nested = [[1, 2], [3, 4], [5]];
final flat = nested.expand((list) => list);
print(flat.toList()); // [1, 2, 3, 4, 5]
// مضاعفة كل عنصر
final numbers = [1, 2, 3];
final duplicated = numbers.expand((n) => [n, n]);
print(duplicated.toList()); // [1, 1, 2, 2, 3, 3]
// every() -- اختبار إذا جميع العناصر تحقق شرطاً
final ages = [25, 30, 18, 42];
print(ages.every((age) => age >= 18)); // true (جميعهم بالغون)
print(ages.every((age) => age >= 21)); // false (18 لا يجتاز)
// any() -- اختبار إذا عنصر واحد على الأقل يحقق شرطاً
final scores = [45, 62, 88, 71];
print(scores.any((s) => s >= 90)); // false (لا درجات A)
print(scores.any((s) => s >= 80)); // true (88 يجتاز)
print(scores.any((s) => s < 50)); // true (45 لا يجتاز)
}
forEach مقابل حلقة for
forEach() تطبق دالة على كل عنصر، لكن لها اختلافات مهمة عن حلقة for العادية.
forEach مقابل حلقة for
void main() {
final names = ['Edrees', 'Ahmed', 'Sara'];
// forEach -- تطبق دالة على كل عنصر
names.forEach((name) => print('Hello, $name!'));
// القيد 1: لا يمكن استخدام break أو continue
// هذا لن يُترجم:
// names.forEach((name) {
// if (name == 'Ahmed') break; // خطأ!
// });
// القيد 2: لا يمكن استخدام await داخل forEach
// هذا لن يعمل كما هو متوقع:
// names.forEach((name) async {
// await Future.delayed(Duration(seconds: 1));
// print(name); // الكل يطبع دفعة واحدة، ليس بالتسلسل!
// });
// استخدم حلقة for عندما تحتاج break أو continue أو await
for (final name in names) {
if (name == 'Ahmed') continue; // يعمل!
print(name);
}
// حلقة for مع await
for (final name in names) {
await Future.delayed(Duration(milliseconds: 100));
print(name); // يطبع بالتسلسل مع تأخير
}
}
// القاعدة العامة:
// - استخدم forEach للآثار الجانبية البسيطة (التسجيل، الطباعة)
// - استخدم حلقة for-in لكل شيء آخر (خاصة إذا كنت بحاجة
// break أو continue أو await أو الوصول بالفهرس)
forEach() مع استدعاء async هو خطأ شائع. طريقة forEach لا تنتظر الاستدعاء، لذا جميع العمليات غير المتزامنة تبدأ في وقت واحد بدلاً من التسلسل. استخدم دائماً حلقة for-in عندما تحتاج معالجة غير متزامنة تسلسلية.تركيب الدوال
تركيب الدوال هو عملية دمج دوال بسيطة لبناء دوال أكثر تعقيداً. بدلاً من استدعاء الدوال داخل الدوال، تنشئ دالة جديدة تسلسلها معاً.
تركيب الدوال
// دالة تركيب عامة: تطبق f بعد g
// compose(f, g)(x) = f(g(x))
B Function(A) compose<A, B, C>(
B Function(C) f,
C Function(A) g,
) {
return (A x) => f(g(x));
}
// الأنبوب: تطبق الدوال من اليسار لليمين (أكثر بديهية)
// pipe(f, g)(x) = g(f(x))
C Function(A) pipe<A, B, C>(
B Function(A) first,
C Function(B) second,
) {
return (A x) => second(first(x));
}
void main() {
// دوال بسيطة للتركيب
int doubleIt(int n) => n * 2;
int addTen(int n) => n + 10;
String stringify(int n) => 'Result: $n';
// التركيب: stringify(addTen(doubleIt(5)))
final transform = compose(stringify, compose(addTen, doubleIt));
print(transform(5)); // Result: 20
// 5 → doubleIt → 10 → addTen → 20 → stringify → "Result: 20"
// استخدام طرق التوسيع لسلسلة أنظف
final result = 5
.let(doubleIt) // 10
.let(addTen) // 20
.let(stringify); // "Result: 20"
print(result);
}
// توسيع لسلسلة بأسلوب الأنبوب
extension Pipe<T> on T {
R let<R>(R Function(T) f) => f(this);
}
مثال عملي: خط أنابيب تحويل البيانات
إليك مثال واقعي لاستخدام الدوال عالية الرتبة لبناء خط أنابيب معالجة البيانات، مشابه لما تجده في خدمة خلفية أو أداة تحليل بيانات.
خط أنابيب تحويل البيانات
class Transaction {
final String id;
final String category;
final double amount;
final DateTime date;
final bool isRefund;
Transaction(this.id, this.category, this.amount, this.date, this.isRefund);
@override
String toString() => 'Transaction($id, $category, \$${amount.toStringAsFixed(2)})';
}
void main() {
final transactions = [
Transaction('t1', 'food', 25.50, DateTime(2024, 1, 15), false),
Transaction('t2', 'transport', 12.00, DateTime(2024, 1, 15), false),
Transaction('t3', 'food', 45.00, DateTime(2024, 1, 16), false),
Transaction('t4', 'food', 15.00, DateTime(2024, 1, 16), true),
Transaction('t5', 'entertainment', 60.00, DateTime(2024, 1, 17), false),
Transaction('t6', 'transport', 8.50, DateTime(2024, 1, 17), false),
Transaction('t7', 'food', 35.00, DateTime(2024, 1, 18), false),
];
// خط الأنابيب: تصفية → تحويل → تجميع
// الخطوة 1: استبعاد المرتجعات
// الخطوة 2: الحصول على معاملات الطعام فقط
// الخطوة 3: حساب الإجمالي والمتوسط
final foodTotal = transactions
.where((t) => !t.isRefund) // استبعاد المرتجعات
.where((t) => t.category == 'food') // الطعام فقط
.map((t) => t.amount) // استخراج المبالغ
.fold(0.0, (sum, amount) => sum + amount); // المجموع
print('Food total (excl. refunds): \$${foodTotal.toStringAsFixed(2)}');
// Food total (excl. refunds): $105.50
// التجميع حسب الفئة والمجموع
final byCategory = transactions
.where((t) => !t.isRefund)
.fold<Map<String, double>>(
{},
(map, t) {
map[t.category] = (map[t.category] ?? 0) + t.amount;
return map;
},
);
print('Spending by category: $byCategory');
// {food: 105.5, transport: 20.5, entertainment: 60.0}
// إيجاد فئة الإنفاق الأعلى
final topCategory = byCategory.entries
.reduce((a, b) => a.value > b.value ? a : b);
print('Top category: ${topCategory.key} (\$${topCategory.value.toStringAsFixed(2)})');
// Top category: food ($105.50)
}
مثال عملي: سلسلة البرمجيات الوسيطة
الدوال عالية الرتبة هي أساس أنماط البرمجيات الوسيطة المستخدمة في أطر الويب، وعملاء HTTP، وأنظمة معالجة الأحداث.
نمط سلسلة البرمجيات الوسيطة
// البرمجية الوسيطة تأخذ طلباً ودالة "التالي"
typedef Middleware = Future<Map<String, dynamic>> Function(
Map<String, dynamic> request,
Future<Map<String, dynamic>> Function(Map<String, dynamic>) next,
);
// برمجية وسيطة للتسجيل
Middleware loggingMiddleware() {
return (request, next) async {
print('→ ${request['method']} ${request['path']}');
final stopwatch = Stopwatch()..start();
final response = await next(request);
stopwatch.stop();
print('← ${response['status']} (${stopwatch.elapsedMilliseconds}ms)');
return response;
};
}
// برمجية وسيطة للمصادقة
Middleware authMiddleware(Set<String> validTokens) {
return (request, next) async {
final token = request['token'] as String?;
if (token == null || !validTokens.contains(token)) {
return {'status': 401, 'body': 'Unauthorized'};
}
return next(request);
};
}
// تركيب البرمجيات الوسيطة في معالج واحد
Future<Map<String, dynamic>> Function(Map<String, dynamic>) composeMiddleware(
List<Middleware> middlewares,
Future<Map<String, dynamic>> Function(Map<String, dynamic>) handler,
) {
return middlewares.reversed.fold(
handler,
(next, middleware) => (request) => middleware(request, next),
);
}
// معالج طلب بسيط
Future<Map<String, dynamic>> handleRequest(Map<String, dynamic> request) async {
return {'status': 200, 'body': 'Hello from ${request['path']}'};
}
Future<void> main() async {
final validTokens = {'token_abc', 'token_xyz'};
// بناء سلسلة البرمجيات الوسيطة
final handler = composeMiddleware(
[loggingMiddleware(), authMiddleware(validTokens)],
handleRequest,
);
// طلب مصادق
final response1 = await handler({
'method': 'GET',
'path': '/api/users',
'token': 'token_abc',
});
print('Response: ${response1['body']}');
// طلب غير مصادق
final response2 = await handler({
'method': 'GET',
'path': '/api/secret',
'token': 'invalid',
});
print('Response: ${response2['body']}');
}
مثال عملي: سجل معالجات الأحداث
الدوال عالية الرتبة تجعل أنظمة معالجة الأحداث نظيفة وقابلة للتوسيع.
معالج أحداث بدوال عالية الرتبة
class EventBus {
final _handlers = <String, List<void Function(Map<String, dynamic>)>>{};
// تسجيل معالج (يُرجع دالة إلغاء الاشتراك)
void Function() on(String event, void Function(Map<String, dynamic>) handler) {
(_handlers[event] ??= []).add(handler);
// إرجاع دالة تزيل هذا المعالج
return () {
_handlers[event]?.remove(handler);
};
}
// تسجيل معالج لمرة واحدة
void once(String event, void Function(Map<String, dynamic>) handler) {
late void Function() unsubscribe;
unsubscribe = on(event, (data) {
handler(data);
unsubscribe();
});
}
// إطلاق حدث
void emit(String event, [Map<String, dynamic> data = const {}]) {
final handlers = _handlers[event];
if (handlers != null) {
// إنشاء نسخة لتجنب التعديل أثناء التكرار
for (final handler in List.from(handlers)) {
handler(data);
}
}
}
}
void main() {
final bus = EventBus();
// تسجيل المعالجات
final unsub = bus.on('user:login', (data) {
print('User logged in: ${data['name']}');
});
bus.on('user:login', (data) {
print('Send welcome email to ${data['email']}');
});
// معالج لمرة واحدة
bus.once('user:login', (data) {
print('First login bonus for ${data['name']}!');
});
// إطلاق الأحداث
bus.emit('user:login', {'name': 'Edrees', 'email': 'edrees@example.com'});
print('---');
bus.emit('user:login', {'name': 'Ahmed', 'email': 'ahmed@example.com'});
// إلغاء اشتراك المعالج الأول
unsub();
print('---');
bus.emit('user:login', {'name': 'Sara', 'email': 'sara@example.com'});
}
الملخص
الدوال عالية الرتبة هي في قلب البرمجة الوظيفية في Dart. بمعاملة الدوال ككائنات من الدرجة الأولى، يمكنك كتابة كود قابل لإعادة الاستخدام والتركيب والاختبار بدرجة عالية. أتقن map() و where() و reduce() و fold() لمعالجة المجموعات. استخدم تركيب الدوال والإغلاقات لبناء خطوط الأنابيب وسلاسل البرمجيات الوسيطة وأنظمة الأحداث. تذكر: فضّل حلقات for-in على forEach() عندما تحتاج break أو continue أو await. الدوال عالية الرتبة ليست مجرد تقنية -- إنها طريقة تفكير تؤدي إلى كود Dart أنظف وأسهل صيانة.
.where().map().fold())، كن واعياً أن كل عملية تنشئ مُكرراً كسولاً جديداً. إذا كنت بحاجة لإعادة استخدام نتائج وسيطة، استدعِ .toList() لتحقيق المُكرر. وإلا، السلسلة فعالة لأن التقييم الكسول يعني أن العناصر تُعالج واحداً تلو الآخر عبر السلسلة بأكملها، بدون إنشاء قوائم وسيطة في الذاكرة.