В этой статье мы рассмотрим некоторые функции более высокого порядка и подумаем, как использовать эти функции, чтобы сделать наши программы более выразительными, и в то же время мы также установим баланс между воспринимаемой сложностью программы и выразительностью.Компромисс и баланс.
что такое выразительность
Одна из самых основных концепций программирования заключается в том, что функция может вызывать другие функции.
Когда функция может вызывать другие функции, а функция может вызываться многими другими функциями, мы можем делать много интересных вещей. Отношения «многие ко многим» между функциями могут сделать программы более выразительными, чем отношения «один ко многим». Мы можем дать каждой функции одну ответственность, а затем назвать эту ответственность; мы также можем гарантировать, что есть одна и только одна функция с ответственностью.
Связь функций «многие ко многим» обеспечивает связь «один к одному» между функциями и обязанностями.
Программисты часто говорят, что язык очень выразителен, но не существует единого универсального стандарта, определяющего, что такое выразительность. Большинство программистов согласны с тем, что важной характеристикой языка, делающей его «выразительным», является то, что он позволяет программистам избегать ненужного многословия в своем коде. Если функция имеет несколько обязанностей, это может сделать ее громоздкой и громоздкой. Если одна и та же обязанность должна выполняться несколько раз, это создает избыточность в программе.
Если все функции в программе имеют одну ответственность, и вся ответственность реализуется только один раз одной функцией, программа избегает ненужного многословия.
Таким образом, отношение «многие ко многим» между функциями позволяет писать очень выразительные программы. А программы без этой возможности очень плохо "выразительны".
Обратная сторона: воспринимаемая сложность
Однако с большой силой приходит большая ответственность. Недостаток функциональной связи «многие ко многим» заключается в том, что по мере увеличения размера программы число операций, которые она может выполнять, резко возрастает. «Выразительность» часто конфликтует с «ощутимой сложностью».
Для того, чтобы было легче понять приведенное выше утверждение, давайте нарисуем диаграмму отношений, чтобы подумать об этом по аналогии. Каждая функция — это узел, а отношение вызова между функциями — это соединение. Предполагая, что в программе нет мертвого кода, каждая структурированная программа формирует граф соединений.
При известном количестве узлов количество связных графов, которые можно нарисовать в этих узлах, образует последовательность целых чисел A001187. (Примечание переводчика: Это слишком хардкорная математика, я не понимаю)... (Математика, я не понимаю, перевод опускаю)... Короче, всего 10 функций могут составить более 34 триллионов программных комбинаций ...
Взрыв гибкости программ заставил программистов воздержаться. Преимущества взаимосвязи между функциями и обязанностями сводятся на нет бесконечной сложностью, которая возникает в результате. Только представьте, как сложно разобраться в программах такой сложности.
JavaScript может предоставить инструменты, помогающие решить эту проблему. Его блоки создают пространства имен, модули ES также имеют эту функцию. Вскоре у него появятся свойства частных объектов. (Примечание переводчика: общедоступные и частные свойства классов вошли в черновик State 3)
Пространства имен ограничивают то, что может быть большим графом, небольшими графами, а количество способов соединения каждого маленького графа с другими небольшими графами (модулями) можно контролировать. Таким образом, вы все еще получаете большую картину, но ваша картина гораздо менее компонуема. Таким образом, вам будет легче понять, что он может сделать и как это сделать.
Мы только что описали способ разработки хороших программных систем почти интуитивным способом: предоставляя программистам гибкость, которая приходит с отношениями «многие ко многим» между объектами, позволяя программистам активно определять, как объекты могут быть связаны.
Но обратите внимание, что мы не говорили, что есть какой-то механизм, позволяющий делать и то, и другое одновременно. Нет, мы просто говорим, что есть инструмент, который помогает нам улучшить выразительность, и еще один инструмент, который помогает нам ограничить воспринимаемую сложность наших программ; между ними существует конфликт.
Теперь, когда мы интуитивно понимаем проблему, давайте рассмотрим некоторые функции более высокого порядка. Из этих функций мы пытаемся увидеть, сосуществуют ли выразительность и воспринимаемая сложность.
Функции высшего порядка
Если функция принимает несколько других функций в качестве аргументов и/или возвращает функцию в качестве значения, мы называем такие функции функциями высшего порядка или HOF.Языки, которые поддерживают HOF, также поддерживают функции граждан первого класса, и почти все поддерживают функция динамического создания.
Функции более высокого порядка дают программистам больше способов деконструировать и составлять программы, и в результате у программистов появляется больше способов писать функции ответственности в отношениях один к одному. Давайте посмотрим на пример.
Легенда гласит, что хорошие компании всегда приглашают выпускников на собеседования по программированию.
Например, объединение двух отсортированных списков вместе. Такая проблема не слишком сложна, и есть реальные сценарии применения. Вот наивный ответ:
function merge({ list1, list2 }) {
if (list1.length === 0 || list2.length === 0) {
return list1.concat(list2);
} else {
let atom, remainder;
if (list1[0] < list2[0]) {
atom = list1[0];
remainder = {
list1: list1.slice(1),
list2,
};
} else {
(atom = list2[0]),
(remainder = {
list1,
list2: list2.slice(1),
});
}
const left = atom;
const right = merge(remainder);
return [left, ...right];
}
}
merge({
list1: [1, 2, 5, 8],
list2: [3, 4, 6, 7],
});
//=> [1, 2, 3, 4, 5, 6, 7, 8]
Вот функция, которая суммирует список чисел:
function sum(list) {
if (list.length === 0) {
return 0;
} else {
const [atom, ...remainder] = list;
const left = atom;
const right = sum(remainder);
return left + right;
}
}
sum([42, 3, -1]);
//=> 44
Мы намеренно пишем эти две функции в одной структуре. Эта структура называется линейной рекурсией. Можем ли мы извлечь эту общую структуру?
Линейная рекурсия
Линейно-рекурсивная форма проста:
- Глядя на входное значение функции, можем ли мы извлечь один из элементов этого значения?
- Если нет, то какое значение мы должны вернуть?
- Если это так, то мы поместим это значение в отдельный элемент и остальные элементы.
- поместите оставшиеся элементы в одну и ту же линейную рекурсивную функцию для выполнения, затем
- Установите некоторую связь между ранее разделенными элементами и результатом линейной рекурсии на оставшихся элементах.
Две функции, которые мы только что показали, имеют такую форму, поэтому давайте напишем функцию более высокого порядка для реализации линейной рекурсии. Давайте возьмем одну из функций в качестве примера для извлечения общей части:
function sum(list) {
const indivisible = (list) => list.length === 0;
const value = () => 0;
const divide = (list) => {
const [atom, ...remainder] = list;
return { atom, remainder };
};
const combine = ({ left, right }) => left + right;
if (indivisible(list)) {
return value(list);
} else {
const { atom, remainder } = divide(list);
const left = atom;
const right = sum(remainder);
return combine({ left, right });
}
}
Мы почти подошли к реализации функции высшего порядка, которую мы хотим, и самая важная часть — переименовать несколько переменных:
function myself(input) {
const indivisible = (list) => list.length === 0;
const value = () => 0;
const divide = (list) => {
const [atom, ...remainder] = list;
return { atom, remainder };
};
const combine = ({ left, right }) => left + right;
if (indivisible(input)) {
return value(input);
} else {
const { atom, remainder } = divide(input);
const left = atom;
const right = myself(remainder);
return combine({ left, right });
}
}
Последний шаг - изменить эти постоянные функции на окончательный возвратmyself
Формальные параметры функции:
function linrec({ indivisible, value, divide, combine }) {
return function myself(input) {
if (indivisible(input)) {
return value(input);
} else {
const { atom, remainder } = divide(input);
const left = atom;
const right = myself(remainder);
return combine({ left, right });
}
};
}
const sum = linrec({
indivisible: (list) => list.length === 0,
value: () => 0,
divide: (list) => {
const [atom, ...remainder] = list;
return { atom, remainder };
},
combine: ({ left, right }) => left + right,
});
Теперь мы можем использоватьsum
а такжеmerge
между одинаковыми свойствами. давайте использоватьlinrec
реализоватьmerge
Бар:
const merge = linrec({
indivisible: ({ list1, list2 }) => list1.length === 0 || list2.length === 0,
value: ({ list1, list2 }) => list1.concat(list2),
divide: ({ list1, list2 }) => {
if (list1[0] < list2[0]) {
return {
atom: list1[0],
remainder: {
list1: list1.slice(1),
list2,
},
};
} else {
return {
atom: list2[0],
remainder: {
list1,
list2: list2.slice(1),
},
};
}
},
combine: ({ left, right }) => [left, ...right],
});
Мы можем пойти еще дальше!
бинарная рекурсия
Давайте реализуемbinrec
Функция, реализующая двоичную рекурсию. Мы начали с примера слияния двух отсортированных списков, иmerge
Функции часто используются в сортировках слиянием.
binrec
на самом деле, чемlinrec
проще.linrec
Также разделите входное значение на одиночные и оставшиеся элементы,binrec
Разделите задачу на две части и примените один и тот же алгоритм к обеим частям:
function binrec({ indivisible, value, divide, combine }) {
return function myself(input) {
if (indivisible(input)) {
return value(input);
} else {
let { left, right } = divide(input);
left = myself(left);
right = myself(right);
return combine({ left, right });
}
};
}
const mergeSort = binrec({
indivisible: (list) => list.length <= 1,
value: (list) => list,
divide: (list) => ({
left: list.slice(0, list.length / 2),
right: list.slice(list.length / 2),
}),
combine: ({ left: list1, right: list2 }) => merge({ list1, list2 }),
});
mergeSort([1, 42, 4, 5]);
//=> [1, 4, 5, 42]
Дыра в мозгу больше.На основе бинарной рекурсии мы можем также расширить многомерную рекурсию, то есть разделить задачу на произвольное количество симметричных частей:
function mapWith(fn) {
return function*(iterable) {
for (const element of iterable) {
yield fn(element);
}
};
}
function multirec({ indivisible, value, divide, combine }) {
return function myself(input) {
if (indivisible(input)) {
return value(input);
} else {
const parts = divide(input);
const solutions = mapWith(myself)(parts);
return combine(solutions);
}
};
}
const mergeSort = multirec({
indivisible: (list) => list.length <= 1,
value: (list) => list,
divide: (list) => [
list.slice(0, list.length / 2),
list.slice(list.length / 2),
],
combine: ([list1, list2]) => merge({ list1, list2 }),
});
Существует бесконечное количество функций высшего порядка, которые мы могли бы исследовать, но тех, что я только что показал, достаточно. Вернемся назад и подумаем о выразительности и воспринимаемой сложности.
Взаимосвязь между функциями высшего порядка, выразительностью и сложностью
... (слишком многословно, повторять предыдущее содержание, не переводить)... Если две функции реализуют одну и ту же ответственность, то наша программа недостаточно СУХАЯ (не повторяйтесь) и выражение плохое.
Какое отношение к этому имеют функции высшего порядка? Как мы только что видели,sum
а такжеmerge
В области решения есть разные обязанности: одна — объединять списки, а другая — суммировать списки. Но оба имеют одну и ту же структуру реализации, которая представляет собой линейную рекурсию. Таким образом, они оба отвечают за реализацию линейных рекурсивных алгоритмов.
Абстрагируясь от линейного рекурсивного алгоритма, мы гарантируем, что существует одна и только одна сущность —linrec
-- Отвечает за реализацию линейной рекурсии. Исходя из этого, мы обнаружили, что первоклассные гражданские функции действительно помогли нам добиться большей выразительности за счет создания отношений «многие ко многим» между функциями.
Однако мы также знаем, что использование таких функций более высокого порядка может увеличить воспринимаемую сложность программы, если некоторые особенности языка или архитектурный дизайн не используются для управления функциями в группах. После группировки все еще существуют богатые взаимосвязи между функциями внутри групп, но отношения между группами ограничены.
Один ко многим и многие ко многим
Давайте сравним использованиеbinrec
а такжеmultirec
реализоватьmergeSort
:
const mergeSort1 = binrec({
indivisible: (list) => list.length <= 1,
value: (list) => list,
divide: (list) => ({
left: list.slice(0, list.length / 2),
right: list.slice(list.length / 2),
}),
combine: ({ left: list1, right: list2 }) => merge({ list1, list2 }),
});
const mergeSort2 = multirec({
indivisible: (list) => list.length <= 1,
value: (list) => list,
divide: (list) => [
list.slice(0, list.length / 2),
list.slice(list.length / 2),
],
combine: ([list1, list2]) => merge({ list1, list2 }),
});
Функции, которые мы передаем в linrec и multirec, интересны, поэтому давайте назовем их:
const hasAtMostOne = (list) => list.length <= 1;
const Identity = (list) => list;
const bisectLeftAndRight = (list) => ({
left: list.slice(0, list.length / 2),
right: list.slice(list.length / 2),
});
const bisect = (list) => [
list.slice(0, list.length / 2),
list.slice(list.length / 2),
];
const mergeLeftAndRight = ({ left: list1, right: list2 }) =>
merge({ list1, list2 });
const mergeBisected = ([list1, list2]) => merge({ list1, list2 });
Глядя на имя функции и фактическую функцию функции, вы можете найти некоторые функции, такие какhasAtMostOne
, Identity
а такжеbisect
Это похоже на функцию общего назначения, которую мы все используем при написании нашего текущего приложения или других приложений. Фактически, эти функции действительно можно найти в некоторых библиотеках функций общего назначения. Они выражают общие операции над списками. ([Примечание переводчика]: в Рамдеidentity
Функция та же, что и здесь.identity
функции и подобныеconst always = x => y => x
Совсем не бред, они имеют смысл только в конкретном контексте)
а такжеbisectLeftAndRight
а такжеmergeLiftAndRight
Похоже, у него есть более конкретная цель. Вряд ли они будут использоваться где-то еще.mergeBisected
Затем немного перемешайте, мы можем или не можем использовать это в другом месте.
Как неоднократно подчеркивалось в начале этой статьи, эта функциональная связь «многие ко многим» может помочь нам улучшить выразительность кода и создать отношения «один к одному» между сущностями программы и обязанностями. Например,bisect
отвечает за разделение списка на две части. Мы можем сделать так, чтобы все остальные части кода вызывалиbisect
, вместо того, чтобы реализовывать эту функцию снова и снова.
Чем более общий интерфейс или «поведенческий протокол» предоставляет функция, и чем более целенаправленными и простыми являются обязанности функции, тем сильнее способность функции создавать отношения «многие ко многим». Итак, когда мы пишем что-то вродеmultirec
Такие функции более высокого порядка должны быть спроектированы таким образом, чтобы они получали функции общего назначения в качестве аргументов, и эти функции общего назначения имели бы только простые обязанности.
Мы также можем написать что-то вродеbisectLeftAndRight
а такжеmergeLeftAndRight
эта функция. Когда мы пишем это, в программе есть отношение один ко многим, потому что за исключениемmerge
У них мало общего назначения, кроме того, что они полезны в функциях. Это ограничивает выразительность нашей программы.
К сожалению, это ограничение не обязательно подразумевает последующее снижение воспринимаемой сложности программы. Внимательно прочитав код, мы увидим, чтоbisectLeftAndRight
Эта функция больше нигде в программе не используется. Мы не сможем уменьшить воспринимаемую сложность программы, если не будем дополнительно использовать такие механизмы, как область видимости модуля, чтобы ограничить область действия этих функций и облегчить их обнаружение.
Таким образом, мы можем наблюдать, что некоторые навыки программирования, такие как запись функции, которая является высокоэффективным интерфейсом, или позволяет функцию принимать обязанности комплексных навыков программирования, уменьшит экспрессию программы, но не снизил программу, воспринимающую сложность Отказ
Какое отношение имеют функции более высокого порядка к фреймворкам и библиотекам?
Грубо говоря, фреймворки и библиотеки — это просто наборы классов, функций и прочего кода.Разница в том, что фреймворки предназначены для вызова из нашего кода, а библиотеки предназначены для вызова из нашего кода.
Фреймворки часто ожидают от нас написания функций или других программных объектов с очень специфическими интерфейсами и поведенческими протоколами. Например, Ember требует от нас расширения его базового класса для создания компонентов вместо использования простых классов ES6. Как мы выяснили выше, когда мы пишем конкретный интерфейс, мы ограничиваем выразительность программы, но не уменьшаем сложность программы.
Это означает, что мы пишем код для фреймворка, поэтому автору фреймворка не нужно беспокоиться о создании связи «многие ко многим» между кодом фреймворка и пользовательским кодом. Например, мы не можем использовать примеси JavaScript, фабрики подклассов и советы по методам при написании класса Ember. Нам пришлось использовать специальные инструменты метапрограммирования, предоставляемые Ember, или использовать плагины, разработанные специально для Ember.
Код, ориентированный на фреймворк, больше похож на один-ко-многим, чем на многие-ко-многим, что делает его менее выразительным.
Библиотеки, напротив, предназначены для вызова нашим кодом. Что наиболее важно, библиотеки вызываются очень многими командами с очень разными стилями программирования, что дает авторам библиотек стимул писать функции с общим интерфейсом и простыми обязанностями.
Библиотечно-ориентированный код больше относится ко многим, чем к одному, что делает его более выразительным.
Разве весь код, ориентированный на фреймворк, плох? Не обязательно, это просто компромисс. Фреймворки обеспечивают стандартный способ ведения дел. Фреймворки обещают помочь нам делать больше, особенно сложные вещи.
В идеале, хотя наш код стал бы менее выразительным под рамкой, наша цель — написать меньше кода. Вместо этого мы используем другие средства, чтобы уменьшить воспринимаемую сложность программы.
от нашегоlinrec
, binrec
а такжеmultirec
Исследуйте эти функции высшего порядка, мы находим контрастность компромиссов, рамки и библиотеки специфичных интерфейсов и общий интерфейс.
【Оригинал】From Higher-Order Functions to Libraries And Frameworks
постскриптум
Функции высшего порядка, приведенные в качестве примеров в этой статье, реализованы с использованием рекурсии. в большинстве случаев,merge
а такжеsum
реализуется итеративно. Итак, хороши ли эти примеры?multirec
Каковы сценарии использования множественной рекурсии? Пожалуйста, с нетерпением ждите следующего перевода "Рекурсивная структура данных и обработка изображений"
насчет нас
МыТехническая группа Ant Insurance Experience, от Ant Financial Insurance Group. Мы молодая команда (без бремени исторического стека технологий), текущий средний возраст 92 года (убрать самый высокий балл 8х лет - лидер команды, убрать самый низкий балл 97 лет - брат стажер). Мы поддерживаем практически весь страховой бизнес Ali Group. В 2018 году созданное нами общее сокровище произвело фурор в страховой отрасли, а в 2019 году мы готовили и реализовывали несколько крупных проектов. Теперь, с быстрым развитием бизнес-группы, команда также быстро расширяется.Приглашаем всех мастеров фронтенда присоединиться к нам~
Мы надеемся, что вы обладаете: прочной технической базой, глубокими знаниями в определенной области (узлы/интерактивный маркетинг/визуализация данных и т. д.); способны быстро и непрерывно учиться в процессе обучения; оптимистичны, веселы, живы и общительны.
Если вы заинтересованы в том, чтобы присоединиться к нам, пожалуйста, отправьте свое резюме по электронной почте: ray.hl@antfin.com
Автор этой статьи: Ant Insurance - Experience Technology Group - Kusatsu
Адрес Наггетс:serialcoder