Skip to content

Latest commit

 

History

History
670 lines (452 loc) · 45.1 KB

05_higher_order.md

File metadata and controls

670 lines (452 loc) · 45.1 KB

{{meta {load_files: [«code/scripts.js», «code/chapter/05_higher_order.js», «code/intro.js»], zip: «node/html"}}}

Функції вищого порядку

{{quote {author: «C.A.R. Hoare», title: «1980 ACM Turing Award Lecture», chapter: true}}

{{index «Hoare, C.A.R.»}}

Існує два способи побудови дизайну програмного забезпечення: Один спосіб - зробити його настільки простим, щоб не було очевидних недоліків, а інший - зробити його настільки складним, щоб не було очевидних недоліків.

quote}}

{{figure {url: «img/chapter_picture_5.jpg», alt: «Ілюстрація із зображенням літер та ієрогліфів різних писемностей - латинської, грецької, арабської, давньоєгипетської та інших», chapter: true}}}

{{index «size of program»}}

Велика програма - це дорога програма, і не лише через час, який вона потребує для створення. Розмір майже завжди передбачає ((складність)), а складність збиває з пантелику програмістів. Розгублені програмісти, у свою чергу, вносять у програми помилки (((баги))s). У великій програмі є багато місця, де ці помилки можуть сховатися, що ускладнює їх пошук.

{{index «summing example»}}

Давайте коротко повернемося до двох останніх прикладів програм, наведених у вступі. Перший з них є самодостатнім і має довжину шість рядків.

let total = 0, count = 1;
while (count <= 10) {
  total += count;
  count += 1;
}
console.log(total);

Другий приклад використовує дві зовнішні функції і має довжину в один рядок.

console.log(sum(range(1, 10)));

Який з них з більшою ймовірністю містить помилку?

{{index «size of program»}}

Якщо порахувати розмір визначень функцій сума та діапазон, то друга програма також велика - навіть більша за першу. Але все ж таки, я б сказав, що вона є більш вірогідною.

{{index [абстракція, «з функціями вищого порядку»], «специфічна мова предметної області»}}

Це тому, що розв'язок виражається у ((словнику)), який відповідає задачі, що розв'язується. Підсумовування діапазону чисел не пов'язане з циклами та лічильниками. Це про діапазони та суми.

У визначеннях цієї лексики (функцій «сума» і «діапазон») все ще будуть фігурувати цикли, лічильники та інші супутні деталі. Але оскільки вони виражають простіші концепції, ніж програма в цілому, їх легше зробити правильними.

Абстрагування

У контексті програмування такі словники зазвичай називають ((абстракція))s. Абстракції дають нам можливість говорити про проблеми на вищому (або більш абстрактному) рівні, не відволікаючись на нецікаві деталі.

{{index «аналогія рецепта», «гороховий суп»}}

Як аналогію, порівняйте ці два рецепти горохового супу. Перший виглядає так:

{{quote

Покладіть у каструлю 1 склянку сушеного гороху на одну людину. Додайте води, щоб горох був добре покритий. Залиште горох у воді щонайменше на 12 годин. Вийміть горох з води і покладіть його в каструлю. Додайте 4 склянки води на одну людину. Накрийте каструлю кришкою і варіть горох на повільному вогні протягом двох годин. Візьміть половину цибулини на людину. Наріжте її ножем на шматочки. Додаємо до гороху. Візьміть стебло селери з розрахунку на одну людину. Нарізати ножем на шматочки. Додайте до гороху. Візьміть моркву з розрахунку на одну людину. Наріжте її на шматочки. Ножем! Додайте до гороху. Варіть ще 10 хвилин.

quote}}

А це другий рецепт:

{{quote

На одну особу: 1 склянка сушеного колотого гороху, 4 склянки води, половина нарізаної цибулини, стебло селери та моркви.

Замочити горох на 12 годин. Варити на повільному вогні 2 години. Подрібнюємо та додаємо овочі. Варити ще 10 хвилин.

quote}}

{{індексний словник}}

Другий варіант коротший і простіший для розуміння. Але вам потрібно розуміти ще кілька слів, пов'язаних з кулінарією, таких як замочувати, тушкувати, нарізати і, мабуть, овочі.

При програмуванні ми не можемо покладатися на те, що всі потрібні нам слова вже є у словнику. Таким чином, ми можемо потрапити в патерн першого рецепту - розробити точні кроки, які має виконати комп'ютер, один за одним, не звертаючи уваги на концепції вищого рівня, які вони виражають.

{Абстрагування індексів

У програмуванні корисно вміти помічати, коли ви працюєте на надто низькому рівні абстракції.

Абстрагування повторення

{{індекс [масиву, ітерації]}}

Прості функції, як ми бачили досі, є гарним способом побудови абстракцій. Але іноді вони бувають недостатніми.

{{index «for loop»}}

У програмі часто буває потрібно виконати щось певну кількість разів. Ви можете написати 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);
  }
}

{{index [функція, «вищого порядку»], loop, [функція, «як значення»]}}

{{indexsee «функція вищого порядку», «функція вищого порядку»}}

Але що, якщо ми хочемо зробити щось інше, ніж просто записати числа? Оскільки «робити щось» можна представити у вигляді функції, а функції - це просто значення, ми можемо передати нашу дію як значення функції.

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(`Одиниця ${i + 1}`);
});
console.log(labels);
// → [«Unit 1», «Unit 2», «Unit 3», «Unit 4», «Unit 5»]

{{індекс «тіло циклу», [дужки, тіло], [дужки, аргументи]}}

За структурою це трохи схоже на цикл for - спочатку описується тип циклу, а потім надається тіло. Однак тіло тепер записується як значення функції, яке загорнуте в дужки виклику repeat. Ось чому його потрібно закривати закриваючою дужкою та закриваючою дужкою. У випадках, подібних до цього прикладу, де тіло є одним невеликим виразом, ви також можете опустити дужки і написати цикл в одному рядку.

Функції вищого порядку

{{index [функція, «вищого порядку»], [функція, «як значення»]}}

Функції, які оперують іншими функціями, приймаючи їх як аргументи або повертаючи їх, називаються функціями вищого порядку. Оскільки ми вже бачили, що функції є регулярними величинами, немає нічого особливо дивного у тому, що такі функції існують. Термін походить з ((математики)), де відмінність між функціями та іншими значеннями сприймається більш серйозно.

{{Абстракція індексу}}

Функції вищих порядків дозволяють нам абстрагуватися від дій, а не лише від значень. Вони бувають декількох видів. Наприклад, ми можемо мати функції, які створюють нові функції.

function greaterThan(n) {
  return m => m > n;
}
нехай greaterThan10 = greaterThan(10);
console.log(greaterThan10(11));
// → true

Ми також можемо мати функції, які змінюють інші функції.

function noisy(f) {
  return (...args) => {
    console.log(«виклик з», args);
    let result = f(...args);
    console.log(«викликається з», args, «, повернуто», result);
    return result;
  };
}
noisy(Math.min)(3, 2, 1);
// → виклик з [3, 2, 1]
// → виклик з [3, 2, 1] , повернуто 1

Можна навіть писати функції, які надають нові типи ((потоку управління)).

function unless(test, then) {
  if (!test) then();
}

repeat(3, n => {
  unless(n % 2 == 1, () => {
    console.log(n, «is even»);
  });
});
// → 0 парне
// → 2 парне

{{index [array, methods], [array, iteration], «forEach method»}}

Існує вбудований метод масиву forEach, який надає щось на зразок циклу for/of як функцію вищого порядку.

[«A», «B»].forEach(l => console.log(l));
// → A
// → B

{{id scripts}}

Набір даних скриптів

Однією зі сфер застосування функцій вищих порядків є обробка даних. Для обробки даних нам знадобляться деякі реальні приклади даних. У цій главі буде використано ((набір даних)) про скрипти - ((системи письма)), такі як латиниця, кирилиця або арабська.

Пам'ятаєте ((Юнікод)), систему, яка призначає номер кожному символу у письмовій мові, з Глава ?? Більшість цих символів пов'язані з певним шрифтом. Стандарт містить 140 різних шрифтів, з яких 81 все ще використовується сьогодні, а 59 є історичними.

Хоча я можу вільно читати лише латинські символи, я ціную той факт, що люди пишуть тексти щонайменше 80 іншими системами письма, багато з яких я навіть не впізнаю. Наприклад, ось зразок ((тамільського)) почерку:

{{figure {url: «img/tamil.png», alt: «Рядок вірша тамільським почерком. Символи відносно прості, акуратно розділені, але зовсім не схожі на латинські."}}}.

{{index «SCRIPTS dataset»}}

Приклад ((набір даних)) містить деяку інформацію про 140 скриптів, визначених у Юнікоді. Він доступний у пісочниці кодування для цієї глави [https://eloquentjavascript.net/code#5)]{if book} як зв'язка SCRIPTS. Обв'язка містить масив об'єктів, кожен з яких описує скрипт.

{
  name: «Coptic»,
  ranges: [[994, 1008], [11392, 11508], [11513, 11520]],
  напрям: «ltr»,
  рік: -200,
  живий: несправжній,
  link: «https://en.wikipedia.org/wiki/Coptic_alphabet»
}

Такий об'єкт повідомляє нам назву скрипту, діапазони Unicode, призначені для нього, напрямок, в якому він написаний, (приблизний) час створення, чи використовується він досі, а також посилання на додаткову інформацію. Напрямок може бути ltr для зліва направо, rtl для справа наліво (так пишуть арабські та івритські тексти) або ttb для зверху вниз (як у монгольській писемності).

{{index «slice method»}}

Властивість ranges містить масив символів Unicode ((range))s, кожен з яких є двоелементним масивом, що містить нижню та верхню межі. Будь-які коди символів у межах цих діапазонів призначаються скрипту. Нижня межа ((діапазон)) є інклюзивною (код 994 є коптським символом), а верхня межа не є інклюзивною (код 1008 не є таким).

Фільтрування масивів

{{index [масив, методи], [масив, фільтрація], «метод фільтрації», [функція, «вищого порядку»], «функція-предикат»}}

Якщо ми хочемо знайти скрипти у наборі даних, які все ще використовуються, наступна функція може бути корисною. Вона відфільтровує елементи масиву, які не пройшли перевірку.

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», ...}, ...]

{{index [function, «as value»], [function, application]}}

Функція використовує аргумент на ім'я test, значення функції, щоб заповнити «прогалину» в обчисленнях - процес прийняття рішення про те, які елементи збирати.

{{index «метод фільтрації», «чиста функція», «побічний ефект»}}

Зверніть увагу, що функція filter замість того, щоб видаляти елементи з існуючого масиву, створює новий масив, до якого включаються лише ті елементи, які пройшли перевірку. Ця функція є чистою. Вона не модифікує масив, який їй передається.

Як і forEach, filter є ((стандартним)) методом масиву. У прикладі функція була визначена лише для того, щоб показати, що вона робить всередині. Відтепер ми будемо використовувати її так:

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

{{id map}}

Перетворення з допомогою map

{{index [array, methods], «map method»}}

Скажімо, у нас є масив об'єктів, що представляють скрипти, отриманий шляхом фільтрації масиву SCRIPTS. Натомість нам потрібен масив імен, який легше перевіряти.

{{index [function, «higher-order»]}}

Метод 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

{{index [array, methods], «приклад підсумовування», «метод reduce»}}

Ще одна поширена операція з масивами - це обчислення з них одного значення. Наш повторюваний приклад, підсумовування набору чисел, є прикладом цього. Іншим прикладом є пошук сценарію з найбільшою кількістю символів.

{{indexsee «fold», «reduce method»}}

{{index [function, «higher-order»], «reduce method»}}

Операція вищого порядку, яка представляє цей шаблон, називається reduce (іноді її також називають fold). Вона створює значення шляхом багаторазового взяття одного елемента з масиву і об'єднання його з поточним значенням. Підсумовуючи числа, ви починаєте з нуля і для кожного елемента додаєте його до суми.

Параметрами для «зменшення», окрім масиву, є функція об'єднання та початкове значення. Ця функція трохи менш проста, ніж filter і map, тому подивіться на неї уважніше:

function reduce(array, combine, start) {
  let current = start;
  for (let елемент масиву) {
    current = combine(current, element);
  }
  return current;
}

console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0));
// → 10

{{index «reduce method», «SCRIPTS dataset»}}

Стандартний метод роботи з масивами reduce, якому, звичайно, відповідає ця функція, має додаткову зручність. Якщо ваш масив містить хоча б один елемент, ви можете не вказувати аргумент start. Метод прийме перший елемент масиву як початкове значення і почне зменшувати з другого елемента.

console.log([1, 2, 3, 4].reduce((a, b) => a + b));
// → 10

{{index maximum, «characterCount function»}}

Щоб використати reduce (двічі) для пошуку скрипту з найбільшою кількістю символів, ми можемо написати щось на кшталт цього:

function characterCount(script) {
  return script.ranges.reduce((count, [from, to]) => { } </ p
    повернути count + (to - from);
  }, 0);
}

console.log(SCRIPTS.reduce((a, b) => {
  return characterCount(a) < characterCount(b) ? b : a;
}));
// → {name: «Han», ...}

Функція characterCount зменшує діапазони, призначені скрипту, шляхом підсумовування їх розмірів. Зверніть увагу на використання деструктуризації у списку параметрів функції-зменшувача. Другий виклик reduce потім використовує це для знаходження найбільшого скрипта шляхом багаторазового порівняння двох скриптів і повернення більшого з них.

У стандарті Юнікод для ієрогліфів хань призначено понад 89 000 символів, що робить його найбільшою писемністю у нашому наборі даних. Ієрогліф хань іноді використовується для китайської, японської та корейської мов. Ці мови мають багато спільних ієрогліфів, хоча й пишуть їх по-різному. Консорціум Юнікод (США) вирішив розглядати їх як єдину систему письма, щоб зберегти коди символів. Це називається Ханьська уніфікація і досі викликає невдоволення деяких людей.

Компоновка ## Компоновка

{{індексний цикл, максимум}}

Подумайте, як би ми написали попередній приклад (знаходження найбільшого скрипту) без функцій вищого порядку. Код не набагато гірший.

let biggest = null;
for (let script of SCRIPTS) {
  if (biggest == null ||)
      characterCount(biggest) < characterCount(script)) { if (biggest == null ||)
    biggest = script;
  }
}
console.log(biggest);
// → {name: «Han», ...}

Є ще кілька прив'язок, і програма стала на чотири рядки довшою, але вона все одно дуже читабельна.

{{index «average function», composability, [function, «higher-order»], «filter method», «map method», «reduce method»}}

{{id average_function}}

Абстракції, які надають ці функції, дійсно блищать, коли вам потрібно компонувати операції. Для прикладу, давайте напишемо код, який знаходить середній рік походження для живих і мертвих скриптів у наборі даних.

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, тоді як другий обчислює лише деякі числа, виконуючи менший обсяг роботи. Зазвичай ви можете дозволити собі читабельний підхід, але якщо ви обробляєте величезні масиви і робите це багато разів, менш абстрактний стиль може бути вартий додаткової швидкості.

Рядки та коди символів

{{index «SCRIPTS dataset»}}

Одним з цікавих застосувань цього набору даних може бути з'ясування того, який скрипт використовується у фрагменті тексту. Давайте розглянемо програму, яка робить це.

Пам'ятайте, що кожен скрипт має масив діапазонів кодів символів, пов'язаних з ним. Маючи код символу, ми можемо використати таку функцію, щоб знайти відповідний скрипт (якщо такий є):

{{індекс «деякий метод», «предикатна функція», [масив, методи]}}

function characterScript(code) {
  for (let script of SCRIPTS) {
    if (script.ranges.some(([from, to]) => {
      return code >= from && code < to;
    })) {
      повернути скрипт;
    }
  }
  return null;
}

console.log(characterScript(121));
// → {name: «Latin», ...}

Метод some - це ще одна функція вищого порядку. Він приймає тестову функцію і повідомляє вам, чи повертає ця функція значення true для будь-якого з елементів масиву.

{{id code_units}}

Але як отримати коди символів у рядку?

У Розділі ? я згадував, що JavaScript ((рядок))и кодуються як послідовність 16-бітних чисел. Вони називаються ((одиниця коду))s. Спочатку передбачалося, що код ((Unicode)) ((символ)) вміщується в таку одиницю (що дає трохи більше 65 000 символів). Коли стало зрозуміло, що цього буде недостатньо, багато хто заперечував проти необхідності використання більшого обсягу пам'яті на символ. Для вирішення цієї проблеми було винайдено формат ((UTF-16)), який також використовується в рядках JavaScript. Він описує найпоширеніші символи за допомогою однієї 16-бітної кодової одиниці, а для інших використовує пару з двох таких одиниць.

{{помилка індексу}}

UTF-16 сьогодні, як правило, вважається поганою ідеєю. Здається, що вона майже навмисно створена для того, щоб провокувати помилки. Легко писати програми, які вдають, що одиниці коду і символи - це одне і те ж саме. І якщо у вашій мові не використовуються двоодиницькі символи, це буде виглядати чудово. Але як тільки хтось спробує використати таку програму з менш поширеними (китайськими ієрогліфами), вона зламається. На щастя, з появою ((емодзі)) всі почали використовувати двоосновні символи, і тягар вирішення таких проблем розподілено більш справедливо.

{{index [рядок, довжина], [рядок, індексація], «charCodeAt метод»}}

На жаль, очевидні операції над рядками 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 (Фактичний код емодзі коня)

{{index «codePointAt method»}}

Метод JavaScript charCodeAt повертає одиницю коду, а не повний код символу. Метод codePointAt, доданий пізніше, повертає повний символ Unicode, тому ми можемо використовувати його для отримання символів з рядка. Але аргумент, переданий до codePointAt, все одно є індексом послідовності кодових одиниць. Щоб перебрати всі символи у рядку, нам все одно доведеться вирішувати питання, чи займає символ одну або дві кодові одиниці.

{{індекс «for/of loop», символ}}

У попередній главі я згадував, що цикл for/of також можна використовувати у рядках. Як і codePointAt, цей тип циклу було введено у той час, коли люди гостро усвідомлювали проблеми з UTF-16. Коли ви використовуєте його для циклу над рядком, він повертає вам реальні символи, а не кодові одиниці.

let roseDragon = «🌹🐉»;
for (let char of roseDragon) {
  console.log(char);
}
// → 🌹
// → 🐉

Якщо у вас є символ (який буде рядком з однієї або двох кодових одиниць), ви можете використати codePointAt(0) для отримання його коду.

Розпізнавання тексту

{{index «SCRIPTS dataset», «countBy function», [array, counting]}}

У нас є функція 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++;
    }
  }
  повернути counts;
}

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

Функція countBy очікує на колекцію (все, що можна перебрати за допомогою for/of) і функцію, яка обчислює назву групи для заданого елемента. Вона повертає масив об'єктів, кожен з яких називає групу і повідомляє вам кількість елементів, знайдених у цій групі.

{{index «метод find»}}

Використовує інший метод масиву, find, який перебирає елементи масиву і повертає перший елемент, для якого функція повертає true. Він повертає undefined, якщо не знаходить такого елемента.

{{index «textScripts function», «Chinese characters»}}

Використовуючи 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 «Скриптів не знайдено»;

  return scripts.map(({name, count}) => {
    return `${Math.round(count * 100 / total)}% ${name}`;
  }).join(», »);
}

console.log(textScripts('英国的狗说 «гав», 俄罗斯的狗说 «тяв»'));
// → 61% ханьська, 22% латиниця, 17% кирилиця

{{index «characterScript function», «filter method»}}

Функція спочатку підраховує символи за іменами, використовуючи characterScript для присвоєння їм імен і повертаючись до рядка «none» для символів, які не є частиною жодного скрипту. Виклик filter вилучає запис для «none» з результуючого масиву, оскільки ці символи нас не цікавлять.

{{index «reduce method», «map method», «join method», [array, methods]}}

Щоб мати можливість обчислити ((percentage))s, нам спочатку потрібна загальна кількість символів, що належать скрипту, яку ми можемо обчислити за допомогою reduce. Якщо таких символів не знайдено, функція повертає певний рядок. В іншому випадку, вона перетворює підраховані записи в читабельні рядки за допомогою map, а потім об'єднує їх за допомогою join.

Підсумок

Можливість передавати значення функцій іншим функціям є дуже корисним аспектом JavaScript. Вона дозволяє нам писати функції, які моделюють обчислення з «прогалинами» в них. Код, який викликає ці функції, може заповнити прогалини, передавши значення функції.

Масиви надають ряд корисних методів вищого порядку. Ви можете використовувати forEach для циклічного перебору елементів масиву. Метод filter повертає новий масив, що містить лише ті елементи, які пройшли через функцію ((предикат)). Ви можете перетворити масив, застосувавши до кожного елемента функцію за допомогою map. Ви можете використовувати reduce для об'єднання всіх елементів масиву в одне значення. Метод ome перевіряє, чи відповідає будь-який елемент заданій предикатній функції, а find знаходить перший елемент, який відповідає предикату.

Вправи

Flattening (вирівнювання)

{{index «flattening (exercise)», «reduce method», «concat method», [array, flattening]}}

Використовуйте метод reduce у поєднанні з методом concat для «сплющення» масиву масивів в один масив, який містить всі елементи вихідних масивів.

{{якщо інтерактивно

нехай масиви = [[1, 2, 3], [4, 5], [6]];
// Ваш код тут.
// → [1, 2, 3, 4, 5, 6]

if}}

Ваш власний цикл

{{index «ваш власний цикл (приклад)», «for loop»}}

Напишіть функцію вищого порядку loop, яка забезпечує щось на кшталт інструкції циклу for. Вона повинна приймати значення, тестову функцію, функцію оновлення та основну функцію. На кожній ітерації вона повинна спочатку запускати тестову функцію для поточного значення циклу і зупинятися, якщо вона повертає false. Потім слід викликати основну функцію, передавши їй поточне значення, і, нарешті, викликати функцію оновлення, щоб створити нове значення і почати все з початку.

При визначенні функції ви можете використовувати звичайний цикл для виконання циклу.

{{якщо інтерактивний

// Ваш код тут.

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

if}}

Everything

{{індекс «предикатна функція», «все (вправа)», «кожен метод», «деякий метод», [масив, методи], «оператор &&», «оператор ||»}}

Масиви також мають метод every, аналогічний методу default. Цей метод повертає значення true, коли задана функція повертає значення true для кожного елемента масиву. У певному сенсі, ome - це версія оператора ||, який діє на масиви, а every - це оператор &&.

Реалізуйте every як функцію, яка отримує масив і предикатну функцію як параметри. Напишіть дві версії, одну з використанням циклу, а другу - з використанням методу some.

{{if інтерактивний

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

if}}

{{hint

{{індекс «everything (вправа)», «оцінка короткого замикання», «ключове слово return»}}

Як і оператор &&, метод every може припинити обчислення наступних елементів, як тільки знайде той, що не збігається. Таким чином, циклічна версія може вийти з циклу за допомогою break або return, щойно натрапить на елемент, для якого предикатна функція повертає значення false. Якщо цикл добігає до кінця, не знайшовши такого елемента, ми знаємо, що всі елементи співпали, і ми повинні повернути true.

Для побудови всіх над деякими ми можемо застосувати ((закони Де Моргана)), які стверджують, що a && b дорівнює !(!a || !b). Це можна узагальнити на масиви, де всі елементи масиву збігаються, якщо у масиві немає жодного елемента, який не збігається.

підказка}}

Домінуючий напрямок запису

{{index «SCRIPTS dataset», «direction (writing)», «groupBy function», «dominant direction (exercise)»}}

Напишіть функцію, яка обчислює домінуючий напрямок письма у рядку тексту. Пам'ятайте, що кожен об'єкт сценарію має властивість direction, яка може бути ltr (зліва направо), rtl (справа наліво) або ttb (зверху вниз).

{{index «characterScript function», «countBy function»}}

Домінуючий напрямок - це напрямок більшості символів, з якими пов'язано скрипт. Функції characterScript і countBy, визначені раніше у цій главі, можуть бути тут корисними.

{{якщо інтерактивно

function dominantDirection(text) {
  // Ваш код тут.
}

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

if}}

{{hint

{{індекс «домінуючий напрямок (вправа)», «функція textScripts», «метод фільтрації», «функція characterScript»}}

Ваш розв'язок може бути дуже схожим на першу частину прикладу з textScripts. Вам знову доведеться рахувати символи за критерієм на основі characterScript, а потім відфільтрувати ту частину результату, яка відноситься до нецікавих (нескриптових) символів.

{{index «reduce method»}}

Знайти напрямок з найбільшою кількістю символів можна за допомогою reduce. Якщо вам незрозуміло, як це зробити, зверніться до прикладу на початку глави, де reduce було використано для пошуку сценарію з найбільшою кількістю символів.

підказка}}