Chapter 5توابع رده‌بالا (Higher-Order)

Tzu-li and Tzu-ssu were boasting about the size of their latest programs. ‘Two-hundred thousand lines,’ said Tzu-li, ‘not counting comments!’ Tzu-ssu responded, ‘Pssh, mine is almost a million lines already.’ Master Yuan-Ma said, ‘My best program has five hundred lines.’ Hearing this, Tzu-li and Tzu-ssu were enlightened.

Master Yuan-Ma, The Book of Programming

There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies.

C.A.R. Hoare, 1980 ACM Turing Award Lecture
Letters from different scripts

یک برنامه بزرگ پرهزینه است و فقط به این خاطر نیست که زمان زیادی برای ساختن آن لازم است. اندازه‌ی برنامه همیشه شامل میزان پیچیدگی برنامه هم می شود، چیزی که برنامه نویسان را سردرگم می کند و موجب می شود اشتباه کنند و برنامه خطا (باگ) داشته باشد. بنابراین یک برنامه‌ی بزرگ فضای زیادی برای پنهان شدن باگ‌ها فراهم می سازد و پیدا کردن باگ‌ها را سخت می کند.

اجازه بدهید به طور مختصر به نسخه‌ی نهایی دو برنامه‌ی نمونه برگردیم که در مقدمه کتاب آورده شده اند. برنامه‌ی اول فاقد تابع و کلا دارای شش خط کد است:

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) از اعداد ربطی به حلقه‌ها و شمارنده‌ها ؛ندارد بلکه مربوط به بازه‌ها و عمل جمع می شود.

در تعریف این واژگان ( توابع sum و range) همچنان از حلقه‌ها، شمارنده‌ها و دیگر جزئیات فرعی استفاده خواهد شد. اما به دلیل اینکه آن‌ها به جای بیان برنامه به عنوان یک کل، مفاهیم ساده‌تری را نشان می دهند، آسان‌تر سامان می یابند.

انتزاع

در فضای برنامه‌نویسی، این گونه واژگان را عموما انتزاع‌ها یا abstractions می گویند. انتزاع‌ها جزئیات را مخفی می کنند و به ما این امکان را می دهند که در باره مسئله‌ها در سطح بالاتری ( انتزاع بیشتر ) گفتگو کنیم.

به عنوان تشبیه، می توان این دو طرز تهیه‌ی سوپ نخود را با هم مقایسه کرد. اولین مورد به صورت زیر خواهد بود:

یک فنجان نخود خشک برای هر نفر درون یک ظرف بریزید. به آن آب اضافه کنید تا همه‌ی نخود‌ها را در بر بگیرد. اجازه بدهید نخودها حداقل 12 ساعت در آب بمانند. بعد نخودها را از آب درآورده درون یک قابلمه قرار دهید. برای هر نفر 4 لیوان آب اضافه کنید. روی قابلمه را پوشانده و بگذارید برای 2 ساعت روی گاز باشد. برای هر نفر نیمی از پیاز را برداشته آن را تکه تکه کنید و به نخود‌ها اضافه نمایید. برای هر نفر یک ساقه‌ی کرفس بردارید. با چاقو قطعه قطعه کنید و به نخود ها اضافه کنید. برای هر نفر یک هویج در نظر گرفته، تکه تکه کرده با چاقو! و به نخودها اضافه کنید. بگذارید 10 دقیقه دیگر بپزد.

طرز تهیه‌ی دوم:

برای هر نفر: یک لیوان نخود خشک، نیمی از یک پیاز تکه تکه شده، یک ساقه کرفس، و یک هویج.

نخود ها را 12 ساعت بخیسانید. 2 ساعت در 4 لیوان آب ( برای یک نفر) روی گاز آهسته بجوشانید. سبزیجات را تکه تکه کرده و اضافه کنید. برای 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) از ریاضیات گرفته شده است جایی که به تمایز بین توابع و دیگر مقادیر اهمیت بیشتری داده شده است.

توابع رده‌بالا، به ما این امکان را می دهند که براساس اقدام‌ها، انتزاع ایجاد کنیم، نه فقط بر اساس مقدارها. این گونه توابع به اشکال مختلفی می آیند. به عنوان نمونه‌، می توانید توابعی داشته باشید که خود توابعی را ایجاد می کنند.

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

مجموعه داده‌ی الفبا

یکی از ناحیه‌هایی که توابع رده‌بالا درخشان عمل می کنند، پردازش داده ها می باشد. برای پردازش داده ها، نیاز به داده‌ی واقعی داریم. در این فصل از مجموعه‌ی داده‌ای در باره‌ی حروف الفبا – سیستم های نوشتاری مانند لاتین، سیریلیک، یا عربی – استفاده می کنیم.

یونیکد را از فصل 1 به خاطر بیاورید، سیستمی که برای هر کاراکتر از زبان های نوشتاری، عددی را اختصاص می داد. اکثر این کاراکترها به الفبای مشخصی تعلق دارند. این استاندارد دارای 140 الفبای متفاوت است – از این تعداد، 81 تای آن ها امروزه استفاده می شوند و 59 مورد دیگر به تاریخ پیوسته اند.

اگرچه من فقط می توانم کاراکترهای لاتین را به صورت روان بخوانم، اما این واقعیت که مردم جهان حداقل به 80 سیستم نوشتاری دیگر می نویسند که خیلی از آن ها را حتی نمی توانم تشخیص دهم را تحسین می کنم . به عنوان مثال، نمونه‌ی زیر دست نوشته ای از زبان تمیل است:

Tamil handwriting

مجموعه‌ی داده نمونه‌ی ما حاوی اطلاعاتی از حدود 140 الفبای موجود در یونیکد است. این داده‌ها در قسمت کدهای این فصل به عنوان متغیر 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 یک کاراکتر قبطی است) و مرز بالایی، خود شامل نمی شود (کد 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، به جای اینکه عناصر را از آرایه حذف کند، آرایه‌ی جدیدی می سازد که شامل فقط عناصری است که با شرط تطبیق دارند .این تابع ناب (pure) است. آرایه‌ای که دریافت می کند را تغییر نمی دهد.

شبیه 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 (کاهش) خوانده می شود ( گاهی اوقات هم آن‌ را تاکردن می نامند.) این متد به صورت مکرر عنصری از آرایه را گرفته و آن را با مقدار قبلی ترکیب کرده و در نهایت یک مقدار واحد تولید می کند. زمانی که اعداد را جمع می کنید، ابتدا با عدد صفر شروع می کنید و بعد یکایک عناصر را به مجموع اضافه می کنید.

پارامترهای تابع 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 بازه‌ های مربوط به یک الفبا را به وسیله جمع کردن اندازه‌ی آن ها کاهش می دهد. به استفاده از “تجزیه” (destructing) در لیست پارامتر ها در تابع کاهش دهنده توجه کنید. در فراخوانی دوم تابع reduce، از این نتیجه برای پیدا کردن بزرگترین الفبا استفاده می شود؛ دو به دو الفبا ها را مقایسه کرده و الفبای بزرگتر را بر می گرداند.

الفبای Han (هان) دارای بیش از 89,000 کاراکتر در استاندارد یونیکد است، که باعث شده به عنوان بزرگ‌ترین سیستم نوشتاری در این مجموعه شناخته شود. هان الفبایی است که (گاهی) برای متون چینی، ژاپنی، و کره‌ای استفاده می شود. این زبان‌ها کاراکترهای مشترک زیادی دارند اگرچه که به شکل متفاوتی آن ها را می نویسند. کنسرسیوم یونیکد ( مستقر در ایالات متحده) تصمیم گرفت که آن ها را به عنوان یک سیستم نوشتاری واحد در نظر بگیرد تا کدهای کارکتر کمتری استفاده شود. این کار یکی سازی الفبای هان خوانده می شود که هنوز بعضی افراد را خیلی عصبانی می کند.

ترکیب پذیری

ملاحظه کنید چگونه بدون استفاده توابع‌ رده‌بالا، می توانستیم مثال قبل را بنویسیم ( پیدا کردن بزرگ‌ترین الفبا). آن‌قدرها هم بد از کار در نمی آمد.

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))));
// → 1188
console.log(Math.round(average(
  SCRIPTS.filter(s => !s.living).map(s => s.year))));
// → 188

بنابراین به طور میانگین الفباهای از رده خارج در یونیکد، قدیمی تر از موارد زنده هستند. این نتیجه خیلی معنای خاصی نمی دهد یا آمار شگفت انگیزی محسوب نمی شود. اما امیدوارم موافق باشید که کدی که برای محاسبه این نتیجه استفاده شده از خوانایی خوبی برخوردار است. می توانید آن را به عنوان یک خط لوله در نظر بگیرید: با همه‌ی الفباها شروع می کنیم، موارد زنده (یا از رده خارج) را فیلتر می کنیم، سال‌ها را از آن ها گرفته، میانگین می گیریم و نتیجه را گرد می کنیم.

قطعا می شد این محاسبه را به وسیله یک حلقه‌ی بزرگ پیاده‌سازی کرد.

let total = 0, count = 0;
for (let script of SCRIPTS) {
  if (script.living) {
    total += script.year;
    count += 1;
  }
}
console.log(Math.round(total / count));
// → 1188

اما کاری که می کند و چگونگی آن به راحتی قابل درک نیست. و به علت اینکه نتایج میانی به عنوان مقادیری مربوط و منسجم نشان داده نمی شوند، برای اینکه بتوان چیزی شبیه به 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 باشد)، تابع نیز true را بر می گرداند.

اما چگونه می توانیم کدهای کاراکتر یک رشته را بدست بیاوریم؟

در فصل 1 اشاره کردم که در جاوااسکریپت رشته‌ها به عنوان دنباله‌ای از اعداد 16 بیتی کدگذاری می شوند که به آن‌ها واحد‌های کد گفته می شود. ابتدا قرار بود در یونیکد هر کد کاراکتر در یکی از این واحد‌ها قرار گیرد ( که چیزی بیش از 65,000 کاراکتر را در اختیار شما می گذارد). زمانی که روشن شد این مقدار کافی نیست، خیلی ها از موضوع اختصاص حافظه‌ی بیشتر برای هر کاراکتر طفره می رفتند. برای حل این مشکل، UTF-16 اختراع شد ، فرمتی که برای رشته‌های جاوااسکریپت استفاده می شود. این سیستم اکثر کاراکترهای رایج را با یک واحد کد 16 بیتی توصیف می کند اما برای دیگر کدها، از دو واحد استفاده می کند.

این‌ روزها UTF-16 عموما به عنوان ایده‌ی بدی شناخته می شود. به نظر می رسد که عمدا طوری طراحی شده است که اشتباه ساز باشد. به‌سادگی توان برنامه‌هایی نوشت که در ظاهر تفاوتی بین واحدها و کاراکترهای کد قائل نمی شوند و بدون مشکل هم کار می کنند. اما به محض اینکه کسی سعی کند از این گونه برنامه‌ها برای نوشتن بعضی کاراکترهای چینی نامتداول استفاده کند، برنامه از کار می افتد. خوشبختانه، با ظهور ایموجی، همه به سراغ استفاده از کاراکترهای دو واحده رفته اند، و مسئولیت سروکار داشتن با این گونه مشکلات با عدالت بیشتری توزیع شده است.

متاسفانه، در جاوااسکریپت کارهای واضح روی رشته‌ها، مثل گرفتن طول آن ها به وسیله خاصیت length و دسترسی به محتوای آن‌ها به وسیله براکت ها، تنها از واحدهای کد پشتیبانی می کند.

// Two emoji characters, horse and shoe
let horseShoe = "🐴👟";
console.log(horseShoe.length);
// → 4
console.log(horseShoe[0]);
// → (Invalid half-character)
console.log(horseShoe.charCodeAt(0));
// → 55357 (Code of the half-character)
console.log(horseShoe.codePointAt(0));
// → 128052 (Actual code for horse emoji)

متد 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 به عنوان آرگومان، یک مجموعه (هرچیزی که بتوان به وسیله‌ی for/of آن را پیمایش کرد) به همراه تابعی برای گروه‌بندی دریافت می کند. خروجی این تابع، آرایه‌ای از اشیاء است که هر یک معرف یک گروه است و به شما می گوید چه تعداد عنصر در آن گروه پیدا شده است.

این تابع خود از متدی دیگر به نام findIndex استفاده می کند. این متد به شکلی شبیه به indexOf عمل می کند، اما به جای گشتن برای یک مقدار خاص، به دنبال اولین مقداری می گردد که تابع داده شده با آن مقدار true را برمی گرداند. مانند indexOf، اگر عنصری با آن شرایط پیدا نشود، -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 مفید است. برای تغییر شکل عناصر یک آرایه به وسیله‌ی یک تابع، می توانید از map استفاده کنید. reduce برای ترکیب همه‌ی عناصر یک آرایه و ساخت یک مقدار واحد، کاربرد دارد. متد some بررسی می کند که آیا حداقل یک عنصر در آرایه با شرط تابع ورودی تطبیق می کند و findIndex موقعیت اولین عنصری که با شرط ارسالی تطبیق دارد را بر می گرداند.

تمرین‌ها

یک‌سطح کردن آرایه

با استفاده از متد reduce و ترکیب آن با concat، آرایه‌ای از آرایه‌ها را گرفته و آرایه‌ی تختی (مسطح) بسازید که شامل همه‌ی عناصر آرایه‌های اصلی باشد.

let arrays = [[1, 2, 3], [4, 5], [6]];
// Your code here.
// → [1, 2, 3, 4, 5, 6]

شبیه‌سازی حلقه

تابع رده‌بالایی به نام loop بنویسید که کاری مشابه یک حلقه‌ی for انجام دهد. این تابع به عنوان آرگومان، یک مقدار، تابع شرط، تابع به‌روز‌رسانی و بدنه‌ی یک تابع را دریافت می کند. در هر تکرار، ابتدا، تابع شرط را بر روی مقدار فعلی حلقه اجرا می کند و در صورتی که false را تولید کرد متوقف می شود. سپس بدنه تابع ارسالی را با دادن مقدار فعلی به آن، اجرا می نماید. در نهایت برای ایجاد یک مقدار جدید، تابع به‌روزرسانی را فراخوانی کرده و از ابتدا شروع می کند.

هنگام تعریف تابع، می توانید از یک حلقه‌ی معمولی برای پیاده‌سازی حلقه‌ی اصلی استفاده کنید.

// Your code here.

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

همه‌چیز

مشابه متد some، آرایه‌ها متدی به نام every نیز دارند. این متد زمانی true برمی گرداند که تابع داده شد برای همه‌ی عناصر آرایه، مقدار true را تولید کند. به نوعی، some نسخه‌ای از عملگر || است که روی آرایه‌ها عمل می کند و every شبیه به عملگر && کار می کند.

متد every را به عنوان یک تابع پیاده سازی کنید که یک آرایه و یک تابع دریافت می کند. دو نسخه‌ از این تابع را بنویسید، یک نسخه با استفاده از حلقه و دیگری با استفاده از متد some.

function every(array, test) {
  // Your code here.
}

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 به محض اینکه به موردی برخورد کند که با شرط تطبیق ندارد، ارزیابی دیگر عناصر را متوقف می کند. بنابراین در نسخه‌ی مبتنی بر حلقه، می توان با مشاهده‌ی خروجی false تابع روی یک عنصر، از حلقه به وسیله‌ی break یا return خارج شد. اگر حلقه بدون برخورد با چنین عنصری به انتهای خود برسد، می دانیم که همه‌ی عناصر مطابق تابع شرط بوده اند و می توانیم true را برگردانیم.

برای ساخت every‍‍ با استفاده از some، می توانیم از قوانین دمورگان استفاده کنیم، که براساس آن‌، a && b برابر است با !(!a || !b). می توان آن را به آرایه‌ها تعمیم داد به این صورت که همه‌ی عناصر در آرایه با شرط تطبیق خواهند داشت اگر عنصری در آرایه نباشد که تطبیق نداشته باشد.

جهت نوشتن غالب

تابعی بنویسید که بتواند جهت نوشت غالب یک متن را محاسبه کند. به یاد دارید که هر شیء الفبا خاصیتی به نام direction دارد که می تواند “ltr” (چپ به راست)، “rtl” (راست به چپ)، یا “ttb” (بالا به پایین) باشد.

جهت نوشتاری غالب جهتی است که بیشتر کاراکترهایی که الفبای مشخصی دارند، در آن جهت نوشته می شوند. احتمالا دو تابع characterScript و countBy که پیش تر نوشته شده اند، اینجا کاربرد خواهند داشت.

function dominantDirection(text) {
  // Your code here.
}

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

پاسخ شما ممکن است شباهت زیادی به نیمه‌ی اول مثال textScripts داشته باشد. دوباره باید کاراکترها را با شرطی براساس characterScript بشمارید و سپس بخشی از نتیجه که الفبای مشخصی ندارند را فیلتر کنید.

پیدا کردن جهت نوشته بر اساس شمارش بیشترین کاراکتر را می توان با متد reduce انجام داد. اگر راه حل به ذهن‌تان نرسید، به مثالی که پیش‌تر در این فصل در مورد استفاده از reduce برای پیدا کردن الفبایی با بیشترین حروف مراجعه کنید.