Зачем писать код с 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. Они позволяют совершать очень много крутых приёмов! Читайте о них больше, пробуйте применять на практике.
Комментарии