اذهب إلى المحتوى

الدوال العليا في جافاسكريبت


أسامة دمراني
اقتباس

قال تزو-لي متبجحًا لتزو-سو أنّ برنامجه الأخير يحتوي على مائتي ألف سطر برمجي خلاف التعليقات، فرد عليه تزو-سو بأن برنامجه به حوالي مليون سطر برمجي! حينئذ قال السيد يوان-ما أنّ أفضل برامجه ليس به سوى خمس مائة سطر فقط، فلما سمعا ذلك عرفا أنّ قولهما ليس بشيء.

__ يوان-ما Yuan-Ma، كتاب البرمجة The Book Of Programming

هناك طريقتان لتصميم البرمجيات، تتمثل إحداهما في جعل البرنامج سهلًا جدًا، بحيث لا تجد فيه أي عيوب، والأخرى بجعله معقدًا جدًا بحيث لا تستطيع رؤية عيوب ظاهرة.

توني هور C. A. R. Hoare، محاضرة جائزة تيورنج، 1980 في رابطة الآلات البرمجية ACM.

chapter_picture_5.jpg

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

لنَعُدْ إلى المثالين الأخيرَين المذكورَين في مقدمة هذه السلسلة، حيث يحتوي المثال الأول منهما على ستة أسطر، وهو مستقل بذاته، انظر كما يلي:

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، وهي تخفي التفاصيل الدقيقة وتعطينا القدرة على الحديث عن المشاكل على مستوى أعلى أو أكثر تجريدًا. انظر هاتين الوصفتين أدناه لتقريب الأمر:

  • الوصفة الأولى:
اقتباس

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

  • الوصفة الثانية:
اقتباس

جهز لكل شخص 1 كوب من البازلاء المجففة، ونصف بصلة مقطعة، وساق من الكرفس وجزرة.

انقع البازلاء في الماء لمدة 12 ساعة، واطهها على نار هادئة جدًا لساعتين مع مقدار 4 أكواب من الماء، وبعدها قطع الخضروات وأضفها، ثم اترك ذلك كله لعشرة دقائق إضافية.

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

يقع الكثير من المبرمجين في خطأ الوصفة الأولى عند سردهم للخطوات الدقيقة والصغيرة التي على الحاسوب تنفيذها خطوةً بخطوة، وذلك بسبب عدم ملاحظتهم للمفاهيم العليا في المشكلة التي بين أيديهم، ويُعَدّ الانتباه عند حلك لمشكلة بهذا الأسلوب مهارةً مفيدةً جدًا.

تجريد التكرار

تُعَدّ الدوال البسيطة طريقة ممتازة لبناء تجريدات، غير أنها تعجز عن ذلك أحيانًا، فمن الشائع أن يفعل البرنامج شيئًا ما بعدد معيّن من المرات باستخدام حلقة 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

كما توفر الدوال أنواعًا جديدةً من تدفق التحكم control flow:

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 منها مهملةً أو تاريخيةً، أي لم تَعُدْ مستخدمة؛ فعلى سبيل المثال، انظر هذه الكتابة من اللغة التاميلية:

tamil.png

تحتوي مجموعة البيانات على بعض أجزاء البيانات من النصوص المئة والأربعين المعرَّفة في اليونيكود، وهي متاحة في صندوق اختبار هذا المقال على صورة رابطة SCRIPTS.

{
  name: "Coptic",
  ranges: [[994, 1008], [11392, 11508], [11513, 11520]],
  direction: "ltr",
  year: -200,
  living: false,
  link: "https://en.wikipedia.org/wiki/Coptic_alphabet"
}

يخبرنا الكائن السابق بكل من: اسم النص، ومجالات اليونيكود المسنَدة إليه، واتجاه الكتابة، والزمن التقريبي لنشأة هذه اللغة، وما إذا كانت مستخدمةً أم لا، ورابط إلى مزيد من التفاصيل والبيانات عنها؛ وقد يكون اتجاه الكتابة من اليسار إلى اليمين "ltr"، أو من اليمين إلى اليسار "rtl"، كما في حالة اللغتين العربية والعبرية، أو من الأعلى إلى الأسفل "ttb" كما في حالة اللغة المنغولية.

تحتوي خاصية ranges على مصفوفة من مجالات المحارف، حيث يكون كل منها مصفوفةً من عنصرين، هما الحدين الأدنى والأعلى، ويُسنَد أيّ رمز للمحارف بين هذه المجالات إلى النص، كما يُضمَّن الحد الأدنى فيها؛ أما الحد الأعلى فلا، أي يُعَدّ رمز 994 محرفًا قبطيًا Coptic في المثال السابق؛ أما الرمز 1008 فلا.

ترشيح المصفوفات

نستخدم دالة filter لإيجاد النصوص واللغات التي مازالت مستخدَمةً في مجموعات البيانات، إذ تُرشِّح عناصر المصفوفة التي لا تجتاز اختبارًا تجريه عليها:

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، وهو قيمة دالةٍ لملء الفراغ "gap" أثناء عملية اختيار العناصر. إذ تلاحظ كيف تبني دالة filter مصفوفةً جديدةً من العناصر التي تجتاز اختبارها بدلًا من حذف العناصر من المصفوفة القديمة، وهذا يشير إلى أنّ هذه الدالة دالةً نقيةً pure function، إذ لا تُعدِّل المصفوفة المُمرَرة إليها.

تشبه هذه الدالة التابع forEach في كونها تابع مصفوفة قياسي، وقد عرَّف المثال السابق الدالة لتوضيح كيفية عملها من الداخل ليس إلا؛ أما من الآن فصاعدًا فسنستخدمها فقط كما يلي:

console.log(SCRIPTS.filter(s => s.direction == "ttb"));
// → [{name: "Mongolian", …}, …]

التحويل مع map

ليكن لدينا مصفوفة كائنات تمثِّل عدة نصوص، حيث أُنتجت بترشيح مصفوفة SCRIPTS، لكننا نريد مصفوفةً من الأسماء لأنها أسهل في البحث والتدقيق.

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

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", …]

وبالمثل، يُعَدّ التابع map تابع مصفوفة قياسي، أي يحاكي كلًا من: filter، وforEach.

التلخيص باستخدام reduce

يُعَدّ إجراء حسابات على قيمة واحدة من المصفوفات من العمليات الشائعة على هذه المصفوفات، ومثالنا التعاودي الذي يستدعي تجميعةً من الأعداد هو مثال على هذا، وكذلك إيجاد النص الحاوي على أكبر عدد من المحارف.

تُدعى العملية العليا الممثِلة لهذا النمط بـ reduce، وتُدعى fold أحيانًا، إذ تُنتِج قيمةً بتكرار أخذ عنصر ما من المصفوفة، ومن ثم جمعه مع القيمة الحالية، فتبدأ عند إيجاد مجموع الأعداد من الصفر، وتضيف كل عنصر إلى المجموع الإجمالي.

تأخذ reduce جزءًا من المصفوفة، ودالة جامعة combining function، وقيمة بدء start value، على أساس معامِلات، وتختلف هذه الدالة عن دالتي 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 المجالات المسندَة إلى نص ما بجمع أحجامها، حيث تلاحظ استخدام التفكيك في قائمة المعامِلات للدالة المقلِّلة، ثم يُستَدعى التابع reduce مرةً ثانية لإيجاد أكبر نص من خلال موازنة نصين في كل مرة وإعادة الأكبر بينهما.

تحتوي لغات الهان -نظام الكتابة الصيني، والياباني، والكوري- على أكثر من 89 ألف محرف مسنَد إليها في معيار يونيكود، مما يجعلها أكبر نظام كتابة في مجموعة البيانات.

وقد قرر مجمع الترميز الموحد Unicode Consortium معاملة تلك اللغات على أنها نظام كتابة واحد لتوفير رموز المحارف، رغم مضايقة هذا لبعض العامة، وسُمي ذلك القرار بتوحيد الهان 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

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

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 لأيّ عنصر في المصفوفة، ولكن كيف سنحصل على رموز المحارف في سلسلة نصية؟

ذكرنا في المقال الأول أنّ سلاسل جافاسكربت النصية مرمّزة على أساس تسلسلات من أعداد 16-بت، وتسمى هذه الأعداد بالأعداد البِتّية لمحارف السلسلة code units، حيث صُممِّت رموز المحارف character code في اليونيكود لتتوافق مع وحدة unit -مثل التي تعطيك 65 ألف محرف-؛ ولكن عارض بعض العامة زيادة الذاكرة المخصصة لكل محرف بعدما تبين عدم كفاية هذا، فقد ابتُكِرت لمعالجة هذه المشكلة صيغة UTF-16 التي استخدمتها جافاسكربت، حيث تصف أكثر المحارف شيوعًا باستخدام عدد بِتّي لمحرف 16-بت واحد، لكنها تستخدم زوجًا من تلك الأعداد البِتّية لغيرها.

تُعَدّ UTF-16 فكرةً سيئةً حاليًا، إذ يبدو أنّها اختُرعت لخلق أخطاء! فمن السهل كتابة برامج تدّعي أنّ الأعداد البِتّية للمحارف والمحارف هما الشيء نفسه، وإن لم تكن لغتك تستخدم محارف من وحدتين فلا بأس؛ لكن سينهار البرنامج عند محاولة أحدهم استخدامه مع المحارف الصينية الأقل شيوعًا، ولحسن الحظ، فقد بدأ الجميع مع اختراع الإيموجي (الرموز التعبيرية) باستخدام المحارف ذات الوحدتين.

لكن العمليات الواضحة في سلاسل جافاسكربت النصية، مثل: الحصول على طولها باستخدام خاصية length، والوصول إلى محتواها باستخدام الأقواس المربعة، لا تتعامل إلا مع الأعداد البِتية للمحارف، أنظر إلى ما يلي:

// محرفي إيموجي، حصان وحذاء
let horseShoe = "??";
console.log(horseShoe.length);
// → 4
console.log(horseShoe[0]);
// → (Invalid half-character)
console.log(horseShoe.charCodeAt(0));
// → 55357 (رمز لنصف محرف)
console.log(horseShoe.codePointAt(0));
// → 128052 (الرمز الحقيقي لرمز الحصان)

يعطينا تابع charCodeAt عداد بِتي للمحرف فقط وليس الرمز الكامل للمحرف؛ أما تابع codePointAt الذي أضيف لاحقًا فيعطي محرف يونيكود كامل، لذا نستطيع استخدامه للحصول على المحارف من سلسلة نصية، لكن لا يزال الوسيط الممرر إلى 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.findIndex(c => c.name == name);
    if (known == -1) {
      counts.push({name, count: 1});
    } else {
      counts[known].count++;
    }
  }
  return counts;
}

console.log(countBy([1, 2, 3, 4, 5], n => n > 2));
// → [{name: false, count: 2}, {name: true, count: 3}]

تتوقع دالة countBy تجميعةً collection -من أي شيء نستطيع التكرار عليه باستخدام for/of-، ودالةً لحساب اسم المجموعة group للعنصر المعطى، حيث تعيد مصفوفةً من الكائنات وكل منها هو اسم لمجموعة، وتخبرك بعدد العناصر الموجودة في تلك المجموعة.

تستخدِم هذه الدالة تابع مصفوفة اسمه findIndex، حيث يحاكي indexof نوعًا ما، لكنه يبحث في القيمة الأولى التي تعيد true في الدالة المعطاة بدلًا من البحث عن قيمة معينة، كما يتشابه معه في إعادة ‎-1 عند عدم وجود مثل هذا العنصر، ونستطيع باستخدام 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.

خاتمة

تبين لنا مما سبق أنّ تمرير قيم دالة ما إلى دوال أخرى مفيد جدًا، إذ يسمح لنا بكتابة دوال تُنمذِج الحسابات التي بها فراغات، إذ تستطيع الشيفرة التي تستدعي هذه الدوال ملء تلك الفراغات بتوفير قيم الدوال؛ أما المصفوفات فتعطينا عددًا من التوابع العليا، ويمكننا استخدام forEach للتكرار على عناصر داخل مصفوفة ما؛ ويعيد تابع filter مصفوفةً جديدةً تحتوي العناصر التي تمرِّر دالة التوقّع predicate function؛ كما نستطيع تحويل مصفوفة ما من خلال وضع كل عنصر في دالة باستخدام map؛ وكذلك نستطيع استخدام reduce لجمع عناصر مصفوفة ما داخل قيمة واحدة؛ أما تابع some فينظر هل ثَمّ عنصر مطابق لدالة توقع معطاة أم لا؛ ويبحث findIndex عن موضع أول عنصر مطابق لتوقّع ما.

تدريبات

التبسيط

استخدم تابع method، وconcat لتبسيط مصفوفة بها مصفوفات أخرى، إلى مصفوفة واحدة بها جميع العناصر الموجودة في تلك المصفوفات كلها.

تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen.

let arrays = [[1, 2, 3], [4, 5], [6]];
// ضع شيفرتك هنا.
// → [1, 2, 3, 4, 5, 6]

الحلقة التكرارية الخاصة بك

اكتب دالة loop العليا التي تعطي شيئًَا مثل تعليمة حلقة for التكرارية، إذ تأخذ قيمةً، ودالة اختبار، ودالة تحديث، ومتن دالة.

تستخدِم في كل تكرار دالة الاختبار أولًا على قيمة التكرار الحالية، وتتوقف إن لم تتحقق -أي أعادت false-، ثم تستدعي متن الدالة لتعطيه القيمة الحالية، وأخيرًا تستدعي دالة التحديث لإنشاء قيمة جديدة والبدء من جديد.

تستطيع عند تعريف الدالة استخدام حلقة تكرارية عادية لتنفيذ التكرار الفعلي.

تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen.

// شيفرتك هنا.

loop(3, n => n > 0, n => n - 1, console.log);
// → 3
// → 2
// → 1

كل شيء

تحتوي المصفوفات على تابع every بالتماثل مع تابع some، ويتحقق every إذا تحققت الدالة المعطاة لكل عنصر في المصفوفة، ويمكن النظر إلى some في سلوكه على المصفوفات على أنه عامِل ||، في حين يكون every عامِل &&.

استخدم every على أساس دالة تأخذ مصفوفة ودالة توقّع على أساس معامِلات، واكتب نسختين، إحداهما باستخدام حلقة تكرارية، والأخرى باستخدام تابع some.

تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen.

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.

نستخدم قوانين دي مورجَن De Morgan لبناء every فوق some، والذي ينص على أنّ a && b تساوي ‎!(!a || !b)‎، ويمكن أن يُعمَّم هذا للمصفوفات، حيث تكون كل العناصر في المصفوفة مطابقةً إذا لم يكن في المصفوفة عنصرًا غير مطابق.

اتجاه الكتابة السائد

اكتب دالة تحسب اتجاه الكتابة السائد في نص ما، وتذكّر أنّه لدى كل كائن من كائنات اللغات خاصية direction، والتي من الممكن أن تكون: ltr، أو rtl، أو ttb، كما ذكرنا في سابق شرحنا هنا.

الاتجاه السائد هو اتجاه أغلب المحارف المرتبطة بلغة ما، وستستفيد من دالتي: characterScript، وcountBy المعرَّفتَين في هذا المقال.

تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen.

function dominantDirection(text) {
  // ضع شيفرتك هنا.
}

console.log(dominantDirection("Hello!"));
// → ltr
console.log(dominantDirection("Hey, مساء الخير"));
// → rtl

إرشادات للحل قد يبدو حلك مثل النصف الأول من مثال textScripts، فعليك عدّ المحارف بمقياس مبني على characterScript، ثم ترشيح الجزء الذي يشير إلى المحارف غير المهمة (غير النصية).

يمكن إيجاد الاتجاه الذي يحمل أعلى عدد من المحارف بواسطة reduce، فإذا لم يكن ذلك واضحًا، فارجع إلى المثال السابق في هذا المقال حيث استُخدِم reduce لإيجاد النص الذي فيه أكثر المحارف.

ترجمة -بتصرف- للفصل الخامس من كتاب Elequent Javascript لصاحبه Marijn Haverbeke.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...