Учитесь писать код без for

Зачем писать код с for, если можно этого не делать? Аргументируем, почему.


Да, это продолжение той самой горячей статьи про написание кода без if!

О чём это

В предыдущей статье приводилось несколько задач, которые нужно было решить без условных операторов. В этой, задач будет немного больше, на решение без циклов.

Под циклами мы имеем в виду императивные циклы наподобие for, for … in, for … of, while, do … while. Все они работают по одному принципу — императивный стиль выполнения операций. Альтернативой ему является декларативный стиль.

Императивность vs декларативность

Это весьма обширная тема, но в двух словах разницу можно описать так:

  • Императивный стиль говорит, “как” ...
  • Декларативный стиль показывает, “что”

Какая между ними разница?

Императивный подход представляет последовательность действий. Сделать то-то, затем это, после что-то ещё. Например: пройтись последовательно по списку чисел, прибавить величину каждого к вычисляющейся сумме.

Декларативный подход показывает, что у нас есть и что нам нужно получить. Например: дан список чисел, нужно получить их сумму.

Императивный язык ближе для компьютера, поскольку выполнять инструкции — и есть то самое, для чего они спроектированы. Декларативный стиль ближе к тому, как мы думаем и интуитивно хотим программировать. Компьютер, сделай это, пожалуйста. Как-нибудь!

К счастью, языки программирования уже предлагают нам средства для декларативного описания императивных инструкций. В этой статье мы сконцентрируемся на декларативном задании циклов.

Сразу скажем, что не нужно искать единственно верный способ программирования. Любая нетривиальная программа почти наверняка должна включать в себя оба подхода. Лучше всего — просто их знать и уверенно пользоваться обоими.

Immutability — неизменяемость

Избегать циклы стоит не только для того, чтобы приобщиться к декларативному стилю. Важный момент здесь — относиться к данным как к immutable.

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

Вместо того, чтобы хранить состояние в переменных для выполнения простых операций, сделайте их неизменяемыми: это безопаснее и чище. Код с неизменяемыми переменными намного проще для работы и расширения.

Читаемость и производительность кода могут быть лучше при таком подходе. Как находить баланс — отдельная тема, достойная обсуждения.

Рекурсия

Ещё один способ избежать цикла — это использовать рекурсию.

Рекурсия проста в исполнении. Создаёте функцию, которая вызывает саму себя (создавая тем самым цикл), добавляете условие выхода.

Не факт, что рекурсию можно отнести к декларативному стилю, но это как минимум альтернатива обычному циклу. Также, рекурсия может быть менее производительна, код менее читаемым.

Иногда рекурсия — лучший способ решить задачу, и мы можем обойти её, воспользовавшись стеком (это несложно).

Задачи

В любом случае, написать что-то без циклов — это просто интересный челлендж!

Здесь приведено несколько задач с решениями, использующими императивные циклы и не использующими. Все примеры написаны на JavaScript.

Какие решения вам нравятся больше, какие легче читаются, как по-вашему?

Задача №1: вычислить сумму чисел в массиве

Предположим, у нас есть массив чисел наподобие следующего:

const arrayOfNumbers = [17, -4, 3.2, 8.9, -1.3, 0, Math.PI];

Решение с циклом:

let sum = 0;
arrayOfNumbers.forEach((number) => {
sum += number;
});
console.log(sum);

Здесь мы для достижения результата постоянно изменяем переменную sum.

Вот решение, использующее прекрасную функцию reduce:

const sum = arrayOfNumbers.reduce((acc, number) =>
 acc + number
);
console.log(sum);

Промежуточные состояния нигде не перезаписываются. Вместо этого, мы сделали множество вызовов функции, а состояние передавалось между этими вызовами до конечного присваивания к sum.

А вот решение с применением рекурсии:

const sum = ([number, ...rest]) => {
if (rest.length === 0) { 
return number;
}
return number + sum(rest);
};
console.log(sum(arrayOfNumbers))

Функция sum вызывает саму себя и использует оператор rest, чтобы уменьшить суммируемый массив. И останавливается, когда массив пуст.

Кому-то может показаться это решение хорошим, но как минимум, оно хуже читается, нежели решение с reduce.

Задача №2: составить предложение из смешанных данных

Допустим, дан массив из string и объектов прочих типов, и нам необходимо склеить все string, игнорируя остальные элементы.

Пример для тестирования:

const dataArray = [0, 'H', {}, 'e', Math.PI, 'l', 'l', 2/9, 'o!'];

Ожидаемый вывод — “Hello!”. Для проверки на тип string, стоит использовать оператор typeof.

Решение с применением простого цикла:

let string = '', i = 0;
while (dataArray[i] !== undefined) {
if (typeof dataArray[i] === 'string') {
string += dataArray[i];
}
i += 1;
}
console.log(string);

А вот решение, использующее функцию filter в комбинации с join:

const string = dataArray.filter(e => typeof e === 'string')
.join('');
console.log(string);

Берите эти функции на заметку! filter изящно скрывает за собой условные операции, и мы получаем новый уровень абстракции.

Задача №3: сделать список объектов из списка значений

Допустим, у нас есть массив из названий книг. Нам необходимо сделать из каждого объект и дать ему уникальный идентификатор.

Пример данных:

const booksArray = [
'Clean Code',
'Code Complete',
'Introduction to Algorithms',
];
// Ожидаемый результат
newArray = [
{ id: 1, title: 'Clean Code' },
{ id: 2, title: 'Code Complete' },
{ id: 3, title: 'Introduction to Algorithms' },
];

Решение с применением обычного цикла:

const newArray = [];
let counter = 1;
for (let title of booksArray) {
 newArray.push({
 id: counter,
 title,
 });
 counter += 1;
}
console.log(newArray);

Решение с простым использованием функции map:

const newArray = booksArray.map((title, index) => ({ 
id: index + 1, 
title 
}));
console.log(newArray);

В итоге

Все решения, предложенные здесь, основаны на функциях map, filter и reduce. Они позволяют совершать очень много крутых приёмов! Читайте о них больше, пробуйте применять на практике.

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ