الفصل 05
الدوال ذات الرتبة الأعلى
هناك طريقتان لبناء تصميم برمجي: إحداهما أن تجعله بسيطاً بحيث لا تكون فيه عيوب ظاهرة، والأخرى أن تجعله معقداً بحيث لا تكون فيه عيوب واضحة.
— C.A.R. Hoare، محاضرة جائزة تورينغ من ACM عام 1980

البرنامج الكبير برنامج مكلف، وليس فقط بسبب الوقت الذي يستغرقه بناؤه. فالحجم ينطوي دائماً تقريباً على تعقيد، والتعقيد يربك المبرمجين. والمبرمجون المرتبكون بدورهم يُدخلون أخطاء (عللاً) في البرامج. عندئذ يوفر البرنامج الكبير مساحة كبيرة تختبئ فيها هذه العلل، مما يجعل العثور عليها صعباً.
لنعد بإيجاز إلى آخر برنامجين مثالين في المقدمة. الأول قائم بذاته وطوله ستة أسطر.
let total = 0, count = 1;
while (count <= 10) {
total += count;
count += 1;
}
console.log(total);
أما الثاني فيعتمد على دالتين خارجيتين وطوله سطر واحد.
console.log(sum(range(1, 10)));
أيّهما أكثر احتمالاً لاحتوائه على علّة؟
إذا حسبنا حجم تعريفَي sum وrange، فالبرنامج الثاني كبير أيضاً — بل أكبر من الأول. لكنني مع ذلك أزعم أنه أكثر احتمالاً لأن يكون صحيحاً.
يعود ذلك إلى أن الحل معبَّر عنه بمفردات تقابل المشكلة التي نحلها. فجمع مدى من الأعداد لا علاقة له بالحلقات والعدادات، بل يتعلق بالمدايات والمجاميع.
ستظل تعريفات هذه المفردات (الدالتان sum وrange) تنطوي على حلقات وعدادات وتفاصيل عارضة أخرى. لكن لأنها تعبّر عن مفاهيم أبسط من البرنامج ككل، فمن الأسهل إتقانها.
التجريد
في سياق البرمجة، تُسمى هذه الأنواع من المفردات عادة تجريدات (abstractions). فالتجريدات تمنحنا القدرة على الحديث عن المشكلات في مستوى أعلى (أو أكثر تجريداً)، دون أن تشتتنا تفاصيل غير مهمة.
وعلى سبيل المقارنة، قارن وصفتَي شوربة البازلاء هاتين. الأولى تقول:
ضع كوباً واحداً من البازلاء المجففة لكل شخص في وعاء. أضف الماء حتى تغمر البازلاء جيداً. اترك البازلاء في الماء 12 ساعة على الأقل. أخرج البازلاء من الماء وضعها في قدر للطهي. أضف 4 أكواب من الماء لكل شخص. غطِّ القدر واترك البازلاء تغلي على نار هادئة لمدة ساعتين. خذ نصف بصلة لكل شخص. قطّعها إلى قطع بسكين. أضفها إلى البازلاء. خذ عود كرفس لكل شخص. قطّعه إلى قطع بسكين. أضفه إلى البازلاء. خذ جزرة لكل شخص. قطّعها إلى قطع. بسكين! أضفها إلى البازلاء. اطبخ 10 دقائق أخرى.
وهذه هي الوصفة الثانية:
لكل شخص: كوب من البازلاء المجففة المنقسمة، و4 أكواب من الماء، ونصف بصلة مفرومة، وعود كرفس، وجزرة. انقع البازلاء 12 ساعة. اغلِها على نار هادئة ساعتين. افرم الخضروات وأضفها. اطبخ 10 دقائق أخرى.
الوصفة الثانية أقصر وأسهل تفسيراً. لكنك تحتاج إلى فهم بضع كلمات إضافية متعلقة بالطهي مثل انقع واغلِ على نار هادئة وافرم، وأظن أيضاً خضروات.
عند البرمجة، لا يمكننا الاعتماد على أن كل الكلمات التي نحتاجها تنتظرنا في القاموس. لذلك قد نقع في نمط الوصفة الأولى — نحدد الخطوات الدقيقة التي على الحاسوب تنفيذها، واحدة تلو الأخرى، عمياً عن المفاهيم الأعلى التي تعبّر عنها.
من المهارات المفيدة في البرمجة أن تلاحظ عندما تعمل في مستوى تجريد منخفض جداً.
تجريد التكرار
الدوال العادية، كما رأيناها حتى الآن، طريقة جيدة لبناء التجريدات. لكنها تقصر أحياناً.
من الشائع أن يفعل البرنامج شيئاً عدداً معيناً من المرات. يمكنك كتابة حلقة for لذلك، هكذا:
for (let i = 0; i < 10; i++) {
console.log(i);
}
هل يمكننا تجريد «فعل شيء ما N مرة» في صورة دالة؟ حسناً، من السهل كتابة دالة تستدعي console.log عدد N من المرات.
function repeatLog(n) {
for (let i = 0; i < n; i++) {
console.log(i);
}
}
لكن ماذا لو أردنا فعل شيء غير تسجيل الأعداد؟ بما أن «فعل شيء ما» يمكن تمثيله بدالة، والدوال مجرد قيم، يمكننا تمرير فعلنا كقيمة دالة.
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log);
// → 0
// → 1
// → 2
لا يلزم تمرير دالة معرَّفة مسبقاً إلى repeat. غالباً ما يكون إنشاء قيمة دالة في الحال أسهل بدلاً من ذلك.
let labels = [];
repeat(5, i => {
labels.push(`Unit ${i + 1}`);
});
console.log(labels);
// → ["Unit 1", "Unit 2", "Unit 3", "Unit 4", "Unit 5"]
هذا مبني قليلاً مثل حلقة for — فهو يصف أولاً نوع الحلقة ثم يقدم جسمها. لكن الجسم الآن مكتوب كقيمة دالة، ملفوفة بين قوسي الاستدعاء إلى repeat. لهذا يجب إغلاقه بالقوس المعقوف الختامي و القوس الهلالي الختامي. وفي حالات كهذا المثال، حيث يكون الجسم تعبيراً صغيراً واحداً، يمكنك أيضاً حذف الأقواس المعقوفة وكتابة الحلقة في سطر واحد.
الدوال ذات رتبة أعلى
الدوال التي تعمل على دوال أخرى، إما بأخذها كمعطيات أو بإرجاعها، تُسمى دوالاً ذات رتبة أعلى (higher-order functions). وبما أننا رأينا بالفعل أن الدوال قيم عادية، فلا شيء لافتاً بشكل خاص في وجود مثل هذه الدوال. المصطلح قادم من الرياضيات، حيث يُؤخذ التمييز بين الدوال وغيرها من القيم على محمل الجد أكثر.
تسمح لنا الدوال ذات رتبة أعلى بالتجريد فوق الأفعال، لا القيم فقط. وهي تأتي في عدة صور. مثلاً، يمكن أن تكون لدينا دوال تُنشئ دوال جديدة.
function greaterThan(n) {
return m => m > n;
}
let greaterThan10 = greaterThan(10);
console.log(greaterThan10(11));
// → true
ويمكن أيضاً أن تكون لدينا دوال تغيّر دوال أخرى.
function noisy(f) {
return (...args) => {
console.log("calling with", args);
let result = f(...args);
console.log("called with", args, ", returned", result);
return result;
};
}
noisy(Math.min)(3, 2, 1);
// → calling with [3, 2, 1]
// → called with [3, 2, 1] , returned 1
ويمكننا حتى كتابة دوال توفر أنواعاً جديدة من تدفق التحكم.
function unless(test, then) {
if (!test) then();
}
repeat(3, n => {
unless(n % 2 == 1, () => {
console.log(n, "is even");
});
});
// → 0 is even
// → 2 is even
ثمة طريقة مدمجة في المصفوفات، forEach، توفر شيئاً مثل حلقة for/of في صورة دالة ذات رتبة أعلى.
["A", "B"].forEach(l => console.log(l));
// → A
// → B
مجموعة بيانات الكتابات
من المجالات التي تتألق فيها الدوال ذات رتبة أعلى معالجة البيانات. ولمعالجة البيانات، سنحتاج إلى بعض البيانات المثال الفعلية. سيستخدم هذا الفصل مجموعة بيانات عن الكتابات — أنظمة كتابة مثل اللاتينية والسيريلية والعربية.
أتذكر Unicode، النظام الذي يخصص عدداً لكل حرف في اللغة المكتوبة، من الفصل الأول؟ معظم هذه الحروف مرتبطة بكتابة معينة. يحتوي المعيار على 140 كتابة مختلفة، منها 81 ما تزال مستخدمة اليوم و59 تاريخية.
رغم أنني لا أقرأ بطلاقة سوى الحروف اللاتينية، فأنا أقدّر حقيقة أن الناس يكتبون نصوصاً بما لا يقل عن 80 نظام كتابة آخر، كثير منها لن أتعرف عليه حتى. مثلاً، هذه عيّنة من الكتابة اليدوية بالتاميلية:

تحتوي مجموعة البيانات المثال على بعض المعلومات عن الكتابات الـ140 المعرَّفة في Unicode. وهي متاحة في بيئة التجربة لهذا الفصل باسم الارتباط SCRIPTS. يحتوي الارتباط على مصفوفة من الكائنات، يصف كل منها كتابة.
{
name: "Coptic",
ranges: [[994, 1008], [11392, 11508], [11513, 11520]],
direction: "ltr",
year: -200,
living: false,
link: "https://en.wikipedia.org/wiki/Coptic_alphabet"
}
يخبرنا مثل هذا الكائن باسم الكتابة، ونطاقات Unicode المخصصة لها، والاتجاه الذي تُكتب به، وزمن نشوئها (التقريبي)، وما إذا كانت ما تزال مستخدمة، ورابطاً لمزيد من المعلومات. وقد يكون الاتجاه "ltr" من اليسار إلى اليمين، أو "rtl" من اليمين إلى اليسار (كما تُكتب النصوص العربية والعبرية)، أو "ttb" من الأعلى إلى الأسفل (كما في الكتابة المنغولية).
تحتوي خاصية ranges على مصفوفة من نطاقات محارف Unicode، كل منها مصفوفة من عنصرين تضم حداً أدنى وحداً أعلى. ويُخصص أي رمز حرف يقع ضمن هذه النطاقات للكتابة. الحد الأدنى شامل (الرمز 994 حرف قبطي) والحد الأعلى غير شامل (الرمز 1008 ليس كذلك).
تصفية المصفوفات
إذا أردنا العثور على الكتابات التي ما تزال مستخدمة في مجموعة البيانات، فقد تكون الدالة التالية مفيدة. فهي تستبعد العناصر التي لا تجتاز اختباراً في مصفوفة.
function filter(array, test) {
let passed = [];
for (let element of array) {
if (test(element)) {
passed.push(element);
}
}
return passed;
}
console.log(filter(SCRIPTS, script => script.living));
// → [{name: "Adlam", …}, …]
تستخدم الدالة المعطى المسمى test، وهو قيمة دالة، لملء «فجوة» في الحساب — عملية تحديد العناصر التي يجب جمعها.
لاحظ كيف تبني دالة filter، بدلاً من حذف عناصر من المصفوفة القائمة، مصفوفة جديدة لا تحتوي إلا العناصر التي تجتاز الاختبار. هذه الدالة نقية، فهي لا تعدّل المصفوفة المعطاة لها.
مثل forEach، فإن filter طريقة قياسية في المصفوفات. عرّف المثال الدالة فقط لنبيّن ما تفعله داخلياً. ومن الآن فصاعداً، سنستخدمها هكذا بدلاً من ذلك:
console.log(SCRIPTS.filter(s => s.direction == "ttb"));
// → [{name: "Mongolian", …}, …]
التحويل باستخدام map
لنفترض أن لدينا مصفوفة من الكائنات تمثل كتابات، ناتجة عن تصفية مصفوفة SCRIPTS بطريقة ما. نريد بدلاً منها مصفوفة من الأسماء، لأن فحصها أسهل.
تحوّل طريقة map مصفوفة بتطبيق دالة على جميع عناصرها وبناء مصفوفة جديدة من القيم المُرجَعة. سيكون طول المصفوفة الجديدة مساوياً لطول المصفوفة المدخلة، لكن محتواها سيكون قد قُوبل إلى صورة جديدة بواسطة الدالة.
function map(array, transform) {
let mapped = [];
for (let element of array) {
mapped.push(transform(element));
}
return mapped;
}
let rtlScripts = SCRIPTS.filter(s => s.direction == "rtl");
console.log(map(rtlScripts, s => s.name));
// → ["Adlam", "Arabic", "Imperial Aramaic", …]
مثل forEach وfilter، فإن map طريقة قياسية في المصفوفات.
التلخيص باستخدام reduce
من الأمور الشائعة الأخرى مع المصفوفات حساب قيمة واحدة منها. ومثالنا المتكرر، جمع مجموعة من الأعداد، حالة من هذا. ومثال آخر هو إيجاد الكتابة التي تضم أكبر عدد من الحروف.
تُسمى العملية ذات الرتبة الأعلى التي تمثل هذا النمط reduce (وتسمى أحياناً أيضاً fold). وهي تبني قيمة بأخذ عنصر واحد من المصفوفة مراراً ودمجه مع القيمة الحالية. عند جمع الأعداد، ستبدأ بالعدد صفر، ولكل عنصر تضيفه إلى المجموع.
وسائط reduce، عدا المصفوفة، هي دالة دمج وقيمة بداية. هذه الدالة أقل بساطة بقليل من filter وmap، فألقِ عليها نظرة فاحصة:
function reduce(array, combine, start) {
let current = start;
for (let element of array) {
current = combine(current, element);
}
return current;
}
console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0));
// → 10
طريقة المصفوفات القياسية reduce، التي تقابل هذه الدالة بالطبع، لها ميزة إضافية. إذا كانت مصفوفتك تحتوي على عنصر واحد على الأقل، يُسمح لك بحذف المعطى start. ستأخذ الطريقة العنصر الأول من المصفوفة كقيمة بداية وتبدأ التقليل من العنصر الثاني.
console.log([1, 2, 3, 4].reduce((a, b) => a + b));
// → 10
لاستخدام reduce (مرتين) لإيجاد الكتابة التي تضم أكبر عدد من الحروف، يمكننا كتابة شيء مثل هذا:
function characterCount(script) {
return script.ranges.reduce((count, [from, to]) => {
return count + (to - from);
}, 0);
}
console.log(SCRIPTS.reduce((a, b) => {
return characterCount(a) < characterCount(b) ? b : a;
}));
// → {name: "Han", …}
تختصر دالة characterCount النطاقات المخصصة لكتابة بجمع أحجامها. لاحظ استخدام التفكيك (destructuring) في قائمة وسائط دالة التقليل. ثم تستخدم ندوة reduce الثانية هذا لإيجاد أكبر كتابة بمقارنة كتابتين مراراً وإرجاع الأكبر.
الكتابة الهانية (Han) مخصص لها أكثر من 89,000 حرف في معيار Unicode، مما يجعلها أكبر نظام كتابة في مجموعة البيانات بفارق كبير. والهانية كتابة تُستخدم أحياناً للنصوص الصينية واليابانية والكورية. تتشارك هذه اللغات كثيراً من الحروف، وإن كانت تميل إلى كتابتها بشكل مختلف. قررت جمعية Unicode (المقرّة في الولايات المتحدة) التعامل معها كنظام كتابة واحد لتوفير رموز الحروف. ويسمى هذا توحيد الهانية (Han unification)، وما يزال يغضب بعض الناس كثيراً.
قابلية التركيب
تأمل كيف كنا سنكتب المثال السابق (إيجاد أكبر كتابة) بدون الدوال ذات رتبة أعلى. الشيفرة ليست أسوأ بكثير.
let biggest = null;
for (let script of SCRIPTS) {
if (biggest == null ||
characterCount(biggest) < characterCount(script)) {
biggest = script;
}
}
console.log(biggest);
// → {name: "Han", …}
ثمة ارتباطات إضافية قليلة، والبرنامج أطول بأربعة أسطر، لكنه ما يزال مقروءاً جداً.
تتألق التجريدات التي توفرها هذه الدوال حقاً عندما تحتاج إلى تركيب عمليات. ومثالاً على ذلك، لنكتب شيفرة تجد متوسط سنة النشوء للكتابات الحية والميتة في مجموعة البيانات.
function average(array) {
return array.reduce((a, b) => a + b) / array.length;
}
console.log(Math.round(average(
SCRIPTS.filter(s => s.living).map(s => s.year))));
// → 1165
console.log(Math.round(average(
SCRIPTS.filter(s => !s.living).map(s => s.year))));
// → 204
كما ترى، الكتابات الميتة في Unicode أقدم في المتوسط من الحية. هذه إحصاءة ليست ذات مغزى كبير ولا مفاجئة. لكنني آمل أن توافق على أن الشيفرة المستخدمة لحسابها ليست صعبة القراءة. يمكنك رؤيتها كخط أنابيب: نبدأ بجميع الكتابات، ونستبعد الحية (أو الميتة)، ونأخذ السنوات منها، ونحسب متوسطها، ونقرب النتيجة.
يمكنك بالتأكيد أيضاً كتابة هذا الحساب كحلقة واحدة كبيرة.
let total = 0, count = 0;
for (let script of SCRIPTS) {
if (script.living) {
total += script.year;
count += 1;
}
}
console.log(Math.round(total / count));
// → 1165
لكن من الأصعب رؤية ما الذي كان يُحسب وكيف. ولأن النتائج الوسيطة لا تُمثَّل كقيم متماسكة، فسيكون استخلاص شيء مثل average في دالة منفصلة عملاً أكبر بكثير.
ومن حيث ما يفعله الحاسوب فعلاً، فإن المقاربتين مختلفتان أيضاً اختلافاً كبيراً. الأولى تبني مصفوفات جديدة عند تشغيل filter وmap، بينما الثانية تحسب بعض الأعداد فقط، فتبذل عملاً أقل. ويمكنك عادة تحمّل المقاربة المقروءة، لكن إن كنت تعالج مصفوفات ضخمة وتفعل ذلك مرات كثيرة، فقد يستحق النمط الأقل تجريداً سرعته الإضافية.
النصوص ورموز المحارف
من الاستخدامات المثيرة لهذه المجموعة تحديد الكتابة التي يستخدمها نص ما. لنستعرض برنامجاً يفعل ذلك.
تذكّر أن لكل كتابة مصفوفة من نطاقات رموز المحارف المرتبطة بها. وبإعطائنا رمز محرف، يمكننا استخدام دالة كهذه لإيجاد الكتابة المقابلة (إن وُجدت):
function characterScript(code) {
for (let script of SCRIPTS) {
if (script.ranges.some(([from, to]) => {
return code >= from && code < to;
})) {
return script;
}
}
return null;
}
console.log(characterScript(121));
// → {name: "Latin", …}
طريقة some دالة ذات رتبة أعلى أخرى. تأخذ دالة اختبار وتخبرك ما إذا كانت تلك الدالة تُرجع true لأي من عناصر المصفوفة.
لكن كيف نحصل على رموز المحارف في نص؟
ذكرت في الفصل الأول أن نصوص JavaScript مُرمَّزة كتسلسل من أعداد 16-بت. وتسمى هذه وحدات الرموز (code units). كان من المفترض في البداية أن يتسع رمز حرف Unicode لوحدة كهذه (وهذا يمنحك ما يزيد قليلاً على 65,000 حرف). وعندما اتضح أن ذلك لن يكون كافياً، تردد كثيرون من الحاجة إلى استخدام ذاكرة أكبر لكل حرف. ولمعالجة هذه المخاوف، اخترع UTF-16، وهو الترميز الذي تستخدمه نصوص JavaScript أيضاً. يصف معظم الحروف الشائعة بوحدة رموز واحدة من 16 بت، لكنه يستخدم زوجاً من وحدتين لحروف أخرى.
يُعتبر UTF-16 عموماً فكرة سيئة اليوم. يبدو كأنه مصمم عن قصد تقريباً ليدعو إلى الأخطاء. فمن السهل كتابة برامج تتظاهر بأن وحدات الرموز والحروف شيء واحد. وإن كانت لغتك لا تستخدم حروفاً من وحدتين، فسيبدو ذلك عاملاً بشكل جيد. لكن بمجرد أن يحاول أحدهم استخدام مثل هذا البرنامج مع بعض الحروف الصينية الأقل شيوعاً، ينكسر. ولحسن الحظ، مع ظهور الإيموجي، بدأ الجميع باستخدام حروف من وحدتين، فصار عبء التعامل مع هذه المشكلات موزعاً بعدالة أكبر.
لسوء الحظ، فإن العمليات البديهية على نصوص JavaScript، مثل الحصول على طولها عبر خاصية length والوصول إلى محتواها باستخدام الأقواس المربعة، تتعامل مع وحدات الرموز فقط.
// حرفان إيموجي: حصان وحذاء
let horseShoe = "🐴👟";
console.log(horseShoe.length);
// → 4
console.log(horseShoe[0]);
// → (نصف حرف غير صالح)
console.log(horseShoe.charCodeAt(0));
// → 55357 (رمز نصف الحرف)
console.log(horseShoe.codePointAt(0));
// → 128052 (الرمز الفعلي لإيموجي الحصان)
تمنحك طريقة charCodeAt في JavaScript وحدة رموز، لا رمز حرف كاملاً. أما طريقة codePointAt، التي أُضيفت لاحقاً، فتمنحك حرف Unicode كاملاً، فيمكننا استخدامها للحصول على الحروف من نص. لكن المعطى الممرر إلى codePointAt ما يزال فهرساً في تسلسل وحدات الرموز. وللطواف على جميع الحروف في نص، سنظل بحاجة إلى التعامل مع مسألة ما إذا كان الحرف يشغل وحدة رموز واحدة أو وحدتين.
ذكرت في الفصل السابق أنه يمكن استخدام حلقة for/of على النصوص أيضاً. ومثل codePointAt، أُدخل هذا النوع من الحلقات في وقت كان الناس فيه مدركين إدراكاً حاداً لمشكلات UTF-16. وعندما تستخدمها للطواف على نص، فإنها تعطيك حروفاً حقيقية، لا وحدات رموز.
let roseDragon = "🌹🐉";
for (let char of roseDragon) {
console.log(char);
}
// → 🌹
// → 🐉
وإذا كان لديك حرف (وهو نص من وحدة رموز واحدة أو وحدتين)، فيمكنك استخدام codePointAt(0) للحصول على رمزه.
التعرّف على النصوص
لدينا دالة characterScript وطريقة للطواف على الحروف بشكل صحيح. الخطوة التالية هي عدّ الحروف التي تنتمي إلى كل كتابة. وسيكون التجريد التالي للعدّ مفيداً هنا:
function countBy(items, groupName) {
let counts = [];
for (let item of items) {
let name = groupName(item);
let known = counts.find(c => c.name == name);
if (!known) {
counts.push({name, count: 1});
} else {
known.count++;
}
}
return counts;
}
console.log(countBy([1, 2, 3, 4, 5], n => n > 2));
// → [{name: false, count: 2}, {name: true, count: 3}]
تتوقع دالة countBy مجموعة (أي شيء يمكننا الطواف عليه بـfor/of) ودالة تحسب اسم مجموعة لعنصر معطى. وتُرجع مصفوفة من الكائنات، يسمي كل منها مجموعة ويخبرك بعدد العناصر التي وُجدت في تلك المجموعة.
وهي تستخدم طريقة أخرى للمصفوفات، find، التي تطوف على عناصر المصفوفة وتُرجع أول عنصر تُرجع فيه دالة القيمة true. وتُرجع undefined عندما لا تجد مثل هذا العنصر.
باستخدام countBy، يمكننا كتابة الدالة التي تخبرنا بالكتابات المستخدمة في نص ما.
function textScripts(text) {
let scripts = countBy(text, char => {
let script = characterScript(char.codePointAt(0));
return script ? script.name : "none";
}).filter(({name}) => name != "none");
let total = scripts.reduce((n, {count}) => n + count, 0);
if (total == 0) return "No scripts found";
return scripts.map(({name, count}) => {
return `${Math.round(count * 100 / total)}% ${name}`;
}).join(", ");
}
console.log(textScripts('英国的狗说"woof", 俄罗斯的狗说"тяв"'));
// → 61% Han, 22% Latin, 17% Cyrillic
تعدّ الدالة أولاً الحروف بالاسم، باستخدام characterScript لتخصيص اسم لها والرجوع إلى النص "none" للحروف التي ليست جزءاً من أي كتابة. ويُسقط استدعاء filter مدخلة "none" من المصفوفة الناتجة، لأننا غير مهتمين بهذه الحروف.
ولكي نتمكن من حساب النسب المئوية، نحتاج أولاً إلى العدد الإجمالي للحروف التي تنتمي إلى كتابة، ويمكننا حسابه بـreduce. وإذا لم نجد مثل هذه الحروف، تُرجع الدالة نصاً محدداً. وإلا، فتحوّل مدخلات العدّ إلى نصوص مقروءة بـmap ثم تدمجها بـjoin.
الخلاصة
القدرة على تمرير قيم الدوال إلى دوال أخرى جانب مفيد للغاية في JavaScript. فهي تتيح لنا كتابة دوال تمثل حسابات فيها «فجوات». ويمكن للشيفرة التي تستدعي هذه الدوال أن تملأ الفجوات بتقديم قيم دوال.
توفر المصفوفات عدداً من الطرق المفيدة ذات رتبة أعلى. يمكنك استخدام forEach للطواف على عناصر مصفوفة. وتُرجع طريقة filter مصفوفة جديدة تحتوي فقط العناصر التي تجتاز دالة الإسناد (predicate). ويمكنك تحويل مصفوفة بإمرار كل عنصر عبر دالة باستخدام map. ويمكنك استخدام reduce لدمج جميع عناصر مصفوفة في قيمة واحدة. وتختبر طريقة some ما إذا كان أي عنصر يطابق دالة إسناد معطاة، بينما يجد find أول عنصر يطابق إسناداً.
التمارين
التسطيح
استخدم طريقة reduce مع طريقة concat لـ«تسوية» مصفوفة من المصفوفات في مصفوفة واحدة تضم جميع عناصر المصفوفات الأصلية.
let arrays = [[1, 2, 3], [4, 5], [6]];
// شيفرتك هنا.
// → [1, 2, 3, 4, 5, 6]
حلقتك الخاصة
اكتب دالة ذات رتبة أعلى loop توفر شيئاً مثل جملة حلقة for. ينبغي أن تأخذ قيمة، ودالة اختبار، ودالة تحديث، ودالة جسم. وفي كل تكرار، ينبغي أن تشغّل أولاً دالة الاختبار على قيمة الحلقة الحالية وتتوقف إن أرجعت false. ثم تستدعي دالة الجسم، معطيةً إياها القيمة الحالية، وأخيراً تستدعي دالة التحديث لإنشاء قيمة جديدة والبدء من البداية.
عند تعريف الدالة، يمكنك استخدام حلقة عادية للقيام بالطواف الفعلي.
// شيفرتك هنا.
loop(3, n => n > 0, n => n - 1, console.log);
// → 3
// → 2
// → 1
كل شيء
للمصفوفات أيضاً طريقة every مماثلة لطريقة some. وتُرجع هذه الطريقة true عندما تُرجع الدالة المعطاة true لكل عنصر في المصفوفة. وبطريقة ما، some نسخة من المعامل || تعمل على المصفوفات، وevery مثل المعامل &&.
نفّذ every كدالة تأخذ مصفوفة ودالة إسناد كوسيطين. واكتب نسختين، واحدة باستخدام حلقة وأخرى باستخدام طريقة some.
function every(array, test) {
// شيفرتك هنا.
}
console.log(every([1, 3, 5], n => n < 10));
// → true
console.log(every([2, 4, 16], n => n < 10));
// → false
console.log(every([], n => n < 10));
// → true
إظهار التلميح
مثل المعامل &&، يمكن لطريقة every أن تتوقف عن تقييم عناصر أخرى بمجرد أن تجد عنصراً لا يطابق. لذا يمكن للنسخة القائمة على الحلقة أن تقفز خارج الحلقة — بـbreak أو return — بمجرد أن تصادف عنصراً تُرجع فيه دالة الإسناد false. وإذا وصلت الحلقة إلى نهايتها دون العثور على مثل هذا العنصر، علمنا أن جميع العناصر طابقت وينبغي أن نُرجع true.
ولبناء every فوق some، يمكننا تطبيق قوانين دي مورغان، التي تنص على أن a && b تساوي !(!a || !b). ويمكن تعميم ذلك على المصفوفات، حيث تطابق جميع العناصر في المصفوفة إذا لم يكن فيها عنصر لا يطابق.
اتجاه الكتابة السائد
اكتب دالة تحسب اتجاه الكتابة السائد في نص. تذكّر أن لكل كائن كتابة خاصية direction يمكن أن تكون "ltr" (من اليسار إلى اليمين)، أو "rtl" (من اليمين إلى اليسار)، أو "ttb" (من الأعلى إلى الأسفل).
الاتجاه السائد هو اتجاه أغلبية الحروف المرتبطة بكتابة. ومن المحتمل أن تكون دالتا characterScript وcountBy المعرَّفتان سابقاً في الفصل مفيدتين هنا.
function dominantDirection(text) {
// شيفرتك هنا.
}
console.log(dominantDirection("Hello!"));
// → ltr
console.log(dominantDirection("Hey, مساء الخير"));
// → rtl
إظهار التلميح
قد يشبه حلك كثيراً النصف الأول من مثال textScripts. فعليك مجدداً عدّ الحروف وفق معيار قائم على characterScript ثم استبعاد الجزء من النتيجة الذي يشير إلى حروف غير مهمة (بلا كتابة).
ويمكن إيجاد الاتجاه ذي أكبر عدد من الحروف بـreduce. وإن لم يتضح كيف، فارجع إلى المثال السابق في الفصل، حيث استُخدم reduce لإيجاد الكتابة التي تضم أكبر عدد من الحروف.