Что такое функции почему они полезны
Обновл. 1 Июл 2020 |
Теперь, когда мы уже знаем, что такое функции и зачем они нужны, давайте более подробно рассмотрим, почему они так полезны.
Зачем использовать функции?
Начинающие программисты часто спрашивают: «А можно ли обходиться без функций и весь код помещать непосредственно в функцию main()?». Если вашего кода всего 10-20 строчек, то можно. Если же серьезно, то функции предназначены для упрощения кода, а не для его усложнения. Они имеют ряд преимуществ, которые делают их чрезвычайно полезными в нетривиальных программах.
Структура. Как только программы увеличиваются в размере/сложности, сохранять весь код внутри main() становится трудно. Функция — это как мини-программа, которую мы можем записать отдельно от головной программы, не заморачиваясь при этом об остальных частях кода. Это позволяет разбивать сложные задачи на более мелкие и простые, что кардинально снижает общую сложность программы.
Повторное использование. После объявления функции, её можно вызывать много раз. Это позволяет избежать дублирования кода и сводит к минимуму вероятность возникновения ошибок при копировании/вставке кода. Функции также могут использоваться и в других программах, уменьшая объем кода, который нужно писать с нуля каждый раз.
Тестирование. Поскольку функции убирают лишний код, то и тестировать его становится проще. А так как функция — это самостоятельная единица, то нам достаточно протестировать её один раз, чтобы убедиться в её работоспособности, а затем мы можем её повторно использовать много раз без необходимости проводить тестирование (до тех пор, пока не внесём изменения в эту функцию).
Модернизация. Когда нужно внести изменения в программу или расширить её функционал, то функции являются отличным вариантом. С их помощью можно внести изменения в одном месте, чтобы они работали везде.
Абстракция. Для того, чтобы использовать функцию, нам нужно знать её имя, данные ввода, данные вывода и где эта функция находится. Нам не нужно знать, как она работает. Это очень полезно для написания кода, понятного другим (например, Стандартная библиотека С++ и всё, что в ней находится, создана по этому принципу).
Каждый раз, при вызове std::cin или std::cout для ввода или вывода данных, мы используем функцию из Стандартной библиотеки C++, которая соответствует всем указанным выше концепциям.
Эффективное использование функций
Одной из наиболее распространенных проблем, с которой сталкиваются новички, является понимание того, где, когда и как эффективно использовать функции. Вот несколько основных рекомендаций при написании функций:
Рекомендация №1: Код, который появляется более одного раза в программе, лучше переписать в виде функции. Например, если мы получаем данные от пользователя несколько раз одним и тем же способом, то это отличный вариант для написания отдельной функции.
Рекомендация №2: Код, который используется для сортировки чего-либо, лучше записать в виде отдельной функции. Например, если у нас есть список вещей, которые нужно отсортировать — пишем функцию сортировки, куда передаем несортированный список и откуда получаем отсортированный.
Рекомендация №3: Функция должна выполнять одно (и только одно) задание.
Рекомендация №4: Когда функция становится слишком большой, сложной или непонятной — её следует разбить на несколько подфункций. Это называется рефакторингом кода.
При изучении C++ вам предстоит написать много программ, которые будут включать следующие три подзадания:
Получение данных от пользователя.
Обработка данных.
Вывод результата.
Для простых программ (менее, чем 30 строчек кода) частично или все эти три подзадания можно записать в функции main(). Для более сложных программ (или просто для практики) каждое из этих трех подзаданий является хорошим вариантом, чтобы написать отдельные функции.
Новички часто комбинируют обработку ввода и вывод результата в одной функции. Тем не менее, это нарушает правило «одного задания». Функция, которая обрабатывает значение, должна возвращать его в caller, а дальше уже пускай caller сам решает, что ему с ним делать.
Оценить статью:
Загрузка…
Источник
Хороший программист старается делать свои функции чистыми. Если знать, что это такое, можно сойти за своего, а заодно написать читаемый код.
Если вы не совсем понимаете, что такое функция и зачем она нужна — добро пожаловать в наш кат:
Что такое функция
Функция — это мини-программа внутри вашей основной программы, которая делает какую-то одну понятную вещь. Вы однажды описываете, что это за вещь, а потом ссылаетесь на это описание.
Например, вы пишете игру. Каждый раз, когда игрок попадает в цель, убивает врага, делает комбо, заканчивает уровень или падает в лаву, вам нужно добавить или убавить ему очков. Это делается двумя действиями: к старым очкам добавляются новые, на экран выводится новая сумма очков. Допустим, эти действия занимают 8 строк кода.
Допустим, в игре есть 100 ситуаций, когда нужно добавить или убавить очки — для каждого типа врага, преграды, уровня и т. д. Чтобы в каждой из ста ситуаций не писать одни и те же восемь строк кода, вы упаковываете эти восемь строк в функцию. И теперь в ста местах вы пишете одну строку: например, changeScore(10) — число очков повысится на 10.
Если теперь изменить, что происходит в функции changeScore(), то изменения отразятся как бы во всех ста местах, где эта функция вызывается.
Зачем нужны функции
Функции нужны, чтобы заметно упрощать и сокращать код, адаптировать его для разных платформ, делать более отказоустойчивым, легко отлаживать. И вообще порядок в функциях — порядок в голове.
Возьмём тот же пример с подсчётом очков. Что если при добавлении очков нужно не только выводить их на экран, но и записывать в файл? Просто добавляете в определении функции дополнительные команды, связанные с файлами, и они теперь будут исполняться каждый раз, когда функцию снова вызовут в основной программе. Не нужно ползать по всему коду, искать места с добавлением очков и дописывать там про файлы. Меньше ручного труда, меньше опечаток, меньше незакрытых скобок.
А что если нужно не только писать очки в файл, но и следить за рекордом? Пишем новую функцию getHighScore(), которая достаёт откуда-то рекорд по игре, и две другие — setHighScore() и celebrateHighScore() — одна будет перезаписывать рекорд, если мы его побили, а вторая — как-то поздравлять пользователя с рекордом.
language: JavaScript
// Объявляем новую функцию. Она будет называться changeScore и принимать один аргумент, который мы для этого фрагмента назовём howMuch. Дальше мы просто будем подавать в функцию число.
function changeScore(howMuch){
// Прибавим к старым очкам новые
playerScore = playerScore + howMuch;
// Выведем новые очки на экран
$(‘#scoretext’).text(playerScore)
// Узнаем, какой у нас рекорд. Для этого объявим новую переменную highScore, вызовем функцию getHighScore(), запишем её результат в эту переменную
var highScore = getHighScore();
// А теперь сравним, больше ли наши очки, чем рекорд по игре
if(playerScore > highScore){
//Рекорд побит, значит, надо его записать
setHighScore(playerScore, playerName);
// Делаем тут что-то, что обычно делают, когда ты побил рекорд игры. Фейерверки? Музыка? Мигание рекордных очков на экране? Мы не знаем пока, что именно будет делать эта функция, и нам это сейчас неважно
celebrateHighScore();
}
}
Скопировать код
Код скопирован
Теперь при каждом срабатывании changeScore() будет вызывать все остальные функции. И сколько бы раз мы ни вызвали в коде changeScore(), она потянет за собой всё хозяйство автоматически.
Сила ещё в том, что при разборе этой функции нам неважно, как реализованы getHighScore(), setHighScore() и celebrateHighScore(). Они задаются где-то в другом месте кода и в данный момент нас не волнуют. Они могут брать данные с жёсткого диска, писать их в базу данных, издавать звуки и взламывать Пентагон — это будет расписано внутри самих функций в других местах текста.
Без функций трудно повесить действия на какие-либо кнопки в интерфейсе. Например, у вас на сайте есть форма, и при клике на кнопку «Отправить» вы хотите проверять, что данные в форме правильно введены. Вы спокойно описываете где-то в коде функцию validateForm() и вешаете её на нажатие кнопки. Кнопку нажали — функция вызвалась. Не нужно вписывать в кнопку полный текст программы.
А без функции пришлось бы писать огромную программу-валидатор прямо внутри кнопки. Это исполнимо, но код выглядел бы страшно громоздким. Что если у вас на странице три формы, и каждую нужно валидировать?
Хорошо написанные функции резко повышают читаемость кода. Мы можем читать чужую программу, увидеть там функцию getExamScore(username) и знать, что последняя каким-то образом выясняет результаты экзамена по такому-то юзернейму. Как она там устроена внутри, куда обращается и что использует — вам неважно. Для нас это как бы одна простая понятная команда.
Можно написать кучу вспомогательных функций, держать их в отдельном файле и подключать к проекту как библиотеку. Например, вы написали один раз все функции для обработки физики игры и потом подключаете эти функции во все свои игры. В одной — роботы, в другой — пираты, но в обеих одна и та же физика.
Функции — это бесконечная радость. На этом наш экскурс в функции закончен, переходим к чистоте.
Что такое чистые функции
Есть понятие чистых функций. Это значит, что если функции два раза дать на обработку одно и то же значение, она всегда выдаст один и тот же результат и в программе не изменит ничего, что не относится непосредственно к этой функции. То есть у чистой функции предсказуемый результат и нет побочных эффектов.
Вот примеры.
Один и тот же результат
Допустим, мы придумали функцию, которая считает площадь круга по его радиусу: getCircleArea(). Для наших целей мы берём число пи, равное 3,1415, и вписываем в функцию:
language: JavaScript
function getCircleArea(radius){
// Задаём наше местное определение числа пи
var localPi = 3.1415;
// Считаем площадь: пи на радиус в квадрате. Это то же самое, что пи умножить на радиус и ещё раз умножить на радиус
var circleArea = localPi*radius*radius;
// Говорим функции вернуть то, что мы сейчас рассчитали
return circleArea;
}
Скопировать код
Код скопирован
Теперь этой функции надо скормить число, и она выдаст площадь круга:
- getCircleArea(2) всегда выдаст результат 12,6060
- getCircleArea(4) всегда выдаст 50,2640
Разработчик может быть уверен, что эта функция всегда выдаст нужную для его задачи площадь круга и не будет зависеть от каких-либо других вещей в его программе. Эта функция с предсказуемым результатом.
Другой пример. Мы пишем программу-таймер, которая должна издать звук, например, за 10 секунд до конца отведённого ей времени. Чтобы узнать, сколько осталось секунд, нам нужна функция: она выясняет количество секунд между двумя отметками времени. Мы даём ей два времени в каком-то формате, а функция сама неким образом высчитывает, сколько между ними секунд. Как именно она это считает, сейчас неважно. Важно, что она это делает одинаково. Это тоже функция с предсказуемым результатом:
- getInterval(’09:00:00′, ’09:00:12′) всегда выдаст 12
- getInterval(’09:00:00′, ’21:00:00′) всегда выдаст 43 200
А теперь пример похожей функции: она определяет время от текущего до какого-то другого времени. При исполнении эта функция запрашивает текущее время в компьютере, сравнивает с целевым и делает нужные вычисления. При запуске одной и той же функции с разницей в несколько секунд она даст разные результаты:
- getSecondsTo(’23:59:59′) в один момент даст 43 293 секунды,
- а спустя 2 минуты эта же функция getSecondsTo(’23:59:59′) даст 43 173 секунды.
Это функция с непредсказуемым результатом. У неё есть непредсказуемая зависимость, которая может повлиять на работу программы — зависимость от текущего времени на компьютере. Что если во время исполнения у пользователя обнулились часы? Или он сменил часовой пояс? Или при запросе текущего времени происходит ошибка? Или его компьютер не поддерживает отдачу времени?
С точки зрения чистых функций, правильнее будет сначала отдельными функциями получить все внешние зависимости, проверить их и убедиться, что они подходят для нашей работы. И потом уже вызвать функцию с подсчётом интервалов. Что-то вроде такого:
- var now = getCurrentTime();
- var interval = getInterval(now, ’23:59:59′);
Тогда в функции getCurrentTime() можно будет прописать всё хозяйство, связанное с получением нужного времени и его проверкой, а в getInterval() оставить только алгоритм, который считает разницу во времени.
Побочные эффекты
Современные языки программирования позволяют функциям работать не только внутри себя, но и влиять на окружение. Например, функция может вывести что-то на экран, записать на диск, изменить какую-то глобальную переменную. Взломать Пентагон, опять же. Всё это называется побочными эффектами. Хорошие программисты смотрят на них крайне настороженно.
Примерчики!
Мы пишем таск-менеджер. В памяти программы хранятся задачи, у каждой из которых есть приоритет: высокий, средний и низкий. Все задачи свалены в кучу в памяти, а нам надо вывести только те, что с высоким приоритетом.
Можно написать функцию, которая считывает все задачи из памяти, находит нужные и возвращает. При этом на задачи в памяти это не влияет: они как были свалены в кучу, так и остались. Это функция без побочных эффектов.
- getTasksByPriority(‘high’) — вернёт новый массив с приоритетными задачами, не изменив другие массивы. В памяти был один массив, а теперь появится ещё и второй.
А можно написать функцию, которая считывает задачи, находит нужные, стирает их из исходного места и записывает в какое-то новое — например, в отдельный массив приоритетных задач. Получается, будто она физически вытянула нужные задачи из исходного массива. Побочный эффект этой функции — изменение исходного массива задач в памяти.
- pullTasksByPriority(‘high’) — физически вытащит задачи из исходного массива и переместит их в какой-то новый. В старом массиве уменьшится число задач.
- Такие изменения называют мутациями: я вызвал функцию в одном месте, а мутировало что-то в другом.
Программисты настороженно относятся к мутациям, потому что за ними сложно следить. Что если из-за какой-то ошибки функции выполнятся в неправильном порядке и уничтожат важные для программы данные? Или функция выполнится непредсказуемо много раз? Или она застрянет в цикле и из-за мутаций разорвёт память? Или мутация произойдёт не с тем куском программы, который мы изначально хотели?
Вот типичная ошибка, связанная с мутацией. Мы пишем игру, нужно поменять сумму игровых очков. За это отвечает функция changeScore(), которая записывает результат в глобальную переменную playerScore — то есть мутирует эту переменную. Мы случайно, по невнимательности, вызвали эту функцию в двух местах вместо одного, и баллы увеличиваются вдвое. Это баг.
Другая типичная ошибка. Программист написал функцию, которая удаляет из таблицы последнюю строку, потому что был почему-то уверен: строка будет пустой и никому не нужной. Случайно эта функция вызывается в бесконечном цикле и стирает все строки, от последней к первой. Данные уничтожаются. А вот если бы функция не удаляла строку из таблицы, а делала новую таблицу без последней строки, данные бы не пострадали.
Без мутирующих функций, конечно, мы не обойдёмся — нужно и выводить на экран, и писать в файл, и работать с глобальными переменными. Сложно представить программу, в которой вообще не будет мутирующих функций. Но программисты предпочитают выделять такие функции отдельно, тестировать их особо тщательно, и внимательно следить за тем, как они работают. Грубо говоря, если функция вносит изменения в большой важный файл, она должна как минимум проверить корректность входящих данных и сохранить резервную копию этого файла.
Как этим пользоваться
Когда будете писать свою следующую функцию, задайтесь вопросами:
- Нет ли тут каких-то зависимостей, которые могут повести себя непредсказуемо? Не беру ли я данные неизвестно откуда? Что если все эти данные у меня не возьмутся или окажутся не тем, что мне надо? Как защитить программу на случай, если этих данных там не окажется?
- Влияет ли эта функция на данные за её пределами?
И если логика программы позволяет, постарайтесь сделать так, чтобы функция ни от чего не зависела и ни на что за своими пределами не влияла. Тогда код будет более читаемым, а коллеги-программисты сразу увидят, что перед ними вдумчивый разработчик.
Практику веб‑программирования можно получить в Яндекс.Практикуме
Первый курс — бесплатно.
Попробовать
Источник
«Функции высшего порядка» — это одна из тех фраз, которыми часто разбрасываются. Но редко кто может остановиться и объяснить, что это такое. Возможно, вы уже знаете, что называют функциями высшего порядка. Но как мы используем их в реальных проектах? Когда и почему они бывают полезны? Можем ли мы с их помощью манипулировать DOM? Или люди, которые используют эти функции, просто хвастаются? Быть может, они бессмысленно усложняют код?
Раньше я считал, что функции высшего порядка полезны. Теперь я считаю их самым важным свойством JavaScript как языка. Но прежде чем мы это обсудим, давайте сначала разберёмся, что же такое функции высшего порядка. И начнём мы с функций в качестве переменных.
Функции как объекты первого класса
В JavaScript есть не меньше трёх способов (всего их больше) написать новую функцию. Во-первых, можно написать объявление функции:
// Take a DOM element and wrap it in a list item element.
function itemise(el) {
const li = document.createElement(‘li’);
li.appendChild(el);
return li;
}
Надеюсь, вам всё понятно. Также вы, вероятно, знаете, что можно написать выражение функции:
const itemise = function(el) {
const li = document.createElement(‘li’);
li.appendChild(el);
return li;
}
И наконец, есть ещё один способ написать ту же функцию — в качестве стрелочной функции:
const itemise = (el) => {
const li = document.createElement(‘li’);
li.appendChild(el);
return li;
}
В данном случае все три способа равноценны. Хотя это не всегда бывает так, на практике у каждого способа небольшие отличия, связанные с тем, что происходит с магией конкретного ключевого слова и метками в трассах стека.
Но обратите внимание, что последние два примера присваивают функцию переменной. Это выглядит мелочью. Почему бы и не присвоить функцию переменной? Но это очень важно. Функции в JavaScript относятся к «первому классу». Поэтому мы можем:
- Присваивать функции переменным.
- Передавать функции в качестве аргументов другим функциям.
- Возвращать функции из других функций.
Это чудесно, но какое отношение всё это имеет к функциям высшего порядка? Обратите внимание на два последних пункта. Скоро мы к ним вернёмся, а пока давайте разберём несколько примеров.
Мы увидели присвоение функций переменным. А что насчёт передачи их в качестве параметров? Давайте напишем функцию, которую можно использовать с DOM-элементами. Если выполнить document.querySelectorAll(), то в ответ получим не массив, а NodeList. У NodeList нет метода .map(), как у массивов, поэтому напишем так:
// Apply a given function to every item in a NodeList and return an array.
function elListMap(transform, list) {
// list might be a NodeList, which doesn’t have .map(), so we convert
// it to an array.
return […list].map(transform);
}
// Grab all the spans on the page with the class ‘for-listing’.
const mySpans = document.querySelectorAll(‘span.for-listing’);
// Wrap each one inside an <li> element. We re-use the
// itemise() function from earlier.
const wrappedList = elListMap(itemise, mySpans);
Здесь мы передаём функцию itemise в качестве аргумента функции elListMap. Но можем использовать elListMap не только для создания списков. К примеру, с её помощью можно добавить класс в набор элементов:
function addSpinnerClass(el) {
el.classList.add(‘spinner’);
return el;
}
// Find all the buttons with class ‘loader’
const loadButtons = document.querySelectorAll(‘button.loader’);
// Add the spinner class to all the buttons we found.
elListMap(addSpinnerClass, loadButtons);
elLlistMap берёт другую функцию в качестве параметра и преобразует. То есть мы можем использовать elListMap для решения разных задач.
Мы рассмотрели пример передачи функций в качестве параметров. Теперь поговорим о возврате функции из функции. Как это выглядит?
Сначала напишем обычную старую функцию. Нам нужно взять список элементов li и обернуть в ul. Легко:
function wrapWithUl(children) {
const ul = document.createElement(‘ul’);
return […children].reduce((listEl, child) => {
listEl.appendChild(child);
return listEl;
}, ul);
}
А если потом у нас будет куча элементов-параграфов, которые нам захочется обернуть в div? Без проблем, напишем для этого ещё одну функцию:
function wrapWithDiv(children) {
const div = document.createElement(‘div’);
return […children].reduce((divEl, child) => {
divEl.appendChild(child);
return divEl;
}, div);
}
Работает отлично. Однако эти две функции очень похожи, разница лишь в родительском элементе, который мы создали.
Теперь мы могли бы написать функцию, которая берёт два параметра: тип родительского элемента и список дочерних элементов. Но есть и другой вариант. Мы можем создать функцию, возвращающую функцию. Например:
function createListWrapperFunction(elementType) {
// Straight away, we return a function.
return function wrap(children) {
// Inside our wrap function, we can ‘see’ the elementType parameter.
const parent = document.createElement(elementType);
return […children].reduce((parentEl, child) => {
parentEl.appendChild(child);
return parentEl;
}, parent);
}
}
Поначалу это может выглядеть немного сложно, так что давайте разделим код. Мы создали функцию, которая всего лишь возвращает другую функцию. Но эта возвращаемая функция помнит параметр elementType. И потом, когда мы вызываем возвращённую функцию, ей уже известно, какой элемент нужно создавать. Поэтому можно создать wrapWithUl и wrapWithDiv:
const wrapWithUl = createListWrapperFunction(‘ul’);
// Our wrapWithUl() function now ‘remembers’ that it creates a ul element.
const wrapWithDiv = createListWreapperFunction(‘div’);
// Our wrapWithDiv() function now ‘remembers’ that it creates a div element.
Эту хитрость, когда возвращённая функция «помнит» о чём-то, называют замыканием. Подробнее можно почитать о них здесь. Замыкания невероятно удобны, но пока что мы не будем о них думать.
Итак, мы разобрали:
- Присвоение функции переменной.
- Передачу функции в качестве параметра.
- Возвращение функции из другой функции…
В общем, функции первого класса — штука приятная. Но при чём тут функции высшего порядка? Давайте рассмотрим определение.
Что такое функция высшего порядка?
Определение: это функция, которая берёт функцию в качестве аргумента или возвращает функцию в качестве результата.
Знакомо? В JavaScript это функции первого класса. То есть «функции высшего порядка» обладают точно такими же преимуществами. Иными словами, это просто вычурное название для простой идеи.
Примеры функций высшего порядка
Если начать искать, то начинаешь везде замечать функции высшего порядка. Самыми распространёнными являются функции, которые принимают другие функции в качестве параметров.
Функции, принимающие другие функции в качестве параметров
Когда вы передаёте callback, вы используете функцию высшего порядка. Во фронтенд-разработке они встречаются повсеместно. Одна из самых распространённых — метод .addEventListener(). Мы используем его, когда хотим выполнить действия в ответ на какие-то события. К примеру, я хочу сделать кнопку, выдающую предупреждение:
function showAlert() {
alert(‘Fallacies do not cease to be fallacies because they become fashions’);
}
document.body.innerHTML += `<button type=»button» class=»js-alertbtn»>
Show alert
</button>`;
const btn = document.querySelector(‘.js-alertbtn’);
btn.addEventListener(‘click’, showAlert);
Здесь мы создали функцию, показывающую предупреждение, добавили на страницу кнопку и передали функцию showAlert() в качестве аргумента в btn.addEventListener().
Также мы встречаем функции высшего порядка, когда используем методы итерации массивов: например, .map(), .filter() и .reduce(). Как в функции elListMap():
function elListMap(transform, list) {
return […list].map(transform);
}
Также функции высшего порядка помогают работать с задержками и таймингом. Функции setTimeout() и setInterval() помогают управлять тем, когда исполняются функции. Например, если нужно через 30 секунд убрать класс highlight, можно это сделать так:
function removeHighlights() {
const highlightedElements = document.querySelectorAll(‘.highlighted’);
elListMap(el => el.classList.remove(‘highlighted’), highlightedElements);
}
setTimeout(removeHighlights, 30000);
Повторюсь, мы создали функцию и передали её другой функции в качестве аргумента.
Как видите, в JavaScript часто встречаются функции, принимающие другие функции. И вы наверняка их уже используете.
Функции, возвращающие функции
Функции такого вида встречаются не столь часто, как предыдущие. Но они тоже полезны. Один из лучших примеров — функция maybe(). Я адаптировал вариант из книги JavaScript Allongé:
function maybe(fn)
return function _maybe(…args) {
// Note that the == is deliberate.
if ((args.length === 0) || args.some(a => (a == null)) {
return undefined;
}
return fn.apply(this, args);
}
}
Вместо того, чтобы разбираться в работе кода, давайте сначала посмотрим, как его можно применять. Посмотрим опять на функцию elListMap():
// Apply a given function to every item in a NodeList and return an array.
function elListMap(transform, list) {
// list might be a NodeList, which doesn’t have .map(), so we convert
// it to an array.
return […list].map(transform);
}
Что будет, если случайно передать в elListMap() null или неопределённое значение? Мы получим TypeError и падение текущей операции, какой бы она ни была. Избежать этого можно с помощью функции maybe():
const safeElListMap = maybe(elListMap);
safeElListMap(x => x, null);
// ← undefined
Вместо падения функция вернёт undefined. А если бы мы передали это в другую функцию, защищённую maybe(), то в ответ снова получили бы undefined. С помощью maybe()можно защитить любое количество функций, это гораздо проще написания миллиарда выражений if.
Функции, возвращающие функции, также часто встречаются в мире React. Например, connect().
И что дальше?
Мы увидели несколько примеров использования функций высшего порядка. И что дальше? Что они могут дать нам такого, чего мы без них не получим?
Чтобы ответить на этот вопрос, давайте рассмотрим ещё один пример — встроенный метод массива .sort(). Да, у него есть недостатки. Он меняет массив вместо того, чтобы возвращать новый. Но давайте пока об этом забудем. Метод .sort() является функцией высшего порядка, он принимает другую функцию в качестве одного из параметров.
Как это работает? Если мы хотим отсортировать массив чисел, сначала нужно создать функцию сравнения:
function compareNumbers(a, b) {
if (a === b) return 0;
if (a > b) return 1;
/* else */ return -1;
}
Теперь отсортируем массив:
let nums = [7, 3, 1, 5, 8, 9, 6, 4, 2];
nums.sort(compareNumbers);
console.log(nums);
// 〕[1, 2, 3, 4, 5, 6, 7, 8, 9]
Можно сортировать и списки чисел. Но какая от этого польза? Насколько часто у нас есть список чисел, который нужно сортировать? Не часто. Обычно мне нужно сортировать массив объектов:
let typeaheadMatches = [
{
keyword: ‘bogey’,
weight: 0.25,
matchedChars: [‘bog’],
},
{
keyword: ‘bog’,
weight: 0.5,
matchedChars: [‘bog’],
},
{
keyword: ‘boggle’,
weight: 0.3,
matchedChars: [‘bog’],
},
{
keyword: ‘bogey’,
weight: 0.25,
matchedChars: [‘bog’],
},
{
keyword: ‘toboggan’,
weight: 0.15,
matchedChars: [‘bog’],
},
{
keyword: ‘bag’,
weight: 0.1,
matchedChars: [‘b’, ‘g’],
}
];
Допустим, я хочу отсортировать этот массив по весу каждой записи. Я мог бы написать с нуля новую функцию сортировки. Но зачем, если можно создать новую функцию сравнения:
function compareTypeaheadResult(word1, word2) {
return -1 * compareNumbers(word1.weight, word2.weight);
}
typeaheadMatches.sort(compareTypeaheadResult);
console.log(typeaheadMatches);
// 〕[{keyword: «bog», weight: 0.5, matchedChars: [«bog»]}, … ]
Можно написать функцию сравнения для любого вида массивов. Метод .sort() нам помогает: «Если дадите мне функцию сравнения, я отсортирую любой массив. Не волнуйтесь о его содержимом. Если дадите функцию сортировки, я это отсортирую». Поэтому нам не нужно самостоятельно писать алгоритм сортировки, мы сосредоточимся на гораздо более простой задаче сравнения двух элементов.
Теперь представим, что мы не используем функции высшего порядка. Мы не можем передать функцию методу .sort(). Нам придётся писать новую функцию сортировки каждый раз, когда нужно отсортировать массив другого вида. Или придётся переизобретать то же самое с указателями функций или объектами. В любом случае получится очень неуклюже.
Однако у нас есть функции высшего порядка, которые позволяют отделить функцию сортировки от функции сравнения. Допустим, сообразительный разработчик браузера обновил .sort(), чтобы тот использовал более быстрый алгоритм. Тогда ваш код только выиграет, вне зависимости от того, что находится внутри сортируемых массивов. И эта схема верна для целого набора функций массивов высшего порядка.
Это приводит нас к такой идее. Метод .sort() абстрагирует задачу сортировки от содержимого массива. Это называется «разделение обязанностей» (separation of concerns). Функции высшего порядка позволяют создавать абстракции, которые без них были бы очень громоздки или вообще невозможны. А на создание абстракций приходится 80 % работы программных инженеров.
Когда мы рефакторим код, чтобы убрать повторы, мы создаём абстракции. Видим паттерн и заменяем его абстрактным представлением. В результате код становится более осмысленным и простым в понимании. По крайней мере, такова цель.
Функции высшего порядка — мощный инструмент создания абстракций. И с абстракциями связан целый раздел математики, теория категорий. Точнее, теория категорий посвящена поиску абстракций абстракций. Иными словами, речь идёт о поиске паттернов паттернов. И за последние 70 лет умные программисты позаимствовали оттуда немало идей, которые превратились в свойства языков и библиотеки. Если мы выучим эти паттерны паттернов, то иногда сможем заменять большие куски кода. Или упрощать сложные проблемы до элегантных комбинаций из простых строительных блоков. Эти блоки — функции высшего порядка. Поэтому они столь важны, они дают нам мощный инструмент для борьбы со сложностью нашего кода.
Дополнительные материалы про функции высшего порядка:
- Функции высшего порядка: пятая глава из Eloquent JavaScript.
- Функции высшего порядка: часть серии статей Composing Sofware.
- Функции высшего порядка в JavaScript.
Вероятно, вы уже используете функции высшего порядка. В JavaScript это так легко, что мы даже не задумываемся. Но лучше знать, о чём говорят люди, когда произносят эту фразу. Это же не сложно. Но за простой идеей таится большая сила.
Если вы опытны в функциональном программировании, то могли заметить, что я использовал не чистые функции и некоторые… многословные имена функций. Это не потому, что не слышал про нечистые функции или общие принципы функционального программирования. И я не пишу такой код в production. Я старался подобрать практические примеры, которые будут понятны новичкам. Иногда приходилось идти на компромиссы. Если интересно, то я уже писал о функциональной чистоте и общих принципах функционального программирования.
Источник