Как я могу _читать_ функциональный код JavaScript?

9

Я считаю, что я изучил некоторые / многие / большинство основных концепций, лежащих в основе функционального программирования в JavaScript. Однако у меня возникают проблемы с чтением функционального кода, даже кода, который я написал, и мне интересно, кто-нибудь может дать мне какие-нибудь советы, советы, рекомендации, терминологию и т. Д., Которые могут помочь.

Возьми код ниже. Я написал этот код. Он призван установить процентное сходство между двумя объектами, скажем, {a:1, b:2, c:3, d:3}и {a:1, b:1, e:2, f:2, g:3, h:5}. Я создал код в ответ на этот вопрос о переполнении стека . Поскольку я точно не знал, о каком процентном сходстве спрашивал плакат, я предоставил четыре разных типа:

  • процент ключей в первом объекте, который можно найти во втором,
  • процент значений в первом объекте, который можно найти во втором, включая дубликаты,
  • процент значений в 1-м объекте, который можно найти во 2-м, без дубликатов, и
  • процент пар {ключ: значение} в 1-м объекте, который можно найти во 2-м объекте.

Я начал с достаточно императивного кода, но быстро понял, что эта проблема хорошо подходит для функционального программирования. В частности, я понял, что если бы я мог извлечь функцию или три для каждой из четырех вышеупомянутых стратегий, которые определяли тип функции, которую я хотел сравнить (например, ключи или значения и т. Д.), То я мог бы быть возможность сократить (простите за игру слов) остальную часть кода на повторяемые единицы. Вы знаете, сохраняя это СУХОЙ. Поэтому я переключился на функциональное программирование. Я очень горжусь результатом, я думаю, что он достаточно элегантный, и я понимаю, что хорошо справился.

Тем не менее, даже написав код сам и понимая каждую его часть во время конструирования, когда я теперь оглядываюсь на него, я по-прежнему несколько удивлен, как читать какую-то конкретную строку, так и как "Грок", что на самом деле делает какая-то конкретная половина строки кода. Я обнаружил, что делаю мысленные стрелки, чтобы соединить разные части, которые быстро разлагаются в беспорядок спагетти.

Итак, может ли кто-нибудь сказать мне, как «читать» некоторые из более запутанных фрагментов кода таким образом, который является одновременно и кратким, и это способствует моему пониманию того, что я читаю? Я думаю, что части, которые меня больше всего привлекают, это те, у которых есть несколько жирных стрелок подряд и / или части, у которых есть несколько скобок подряд. Опять же, по своей сути, я могу со временем разобраться в логике, но (я надеюсь) есть лучший способ быстро и четко и прямо «взять на себя» линию функционального программирования на JavaScript.

Не стесняйтесь использовать любую строку кода снизу или даже другие примеры. Однако, если вы хотите, чтобы некоторые первоначальные предложения от меня, вот несколько. Начните с достаточно простого. С ближе к концу кода, есть такой , который передается в качестве параметра функции: obj => key => obj[key]. Как читать и понимать это? Более длинный пример одну полной функции от около начала: const getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key));. Последняя mapчасть меня особенно привлекает.

Пожалуйста, обратите внимание, что в данный момент я не ищу ссылки на Haskell или символьную абстрактную нотацию или основы каррирования и т. Д. То, что я ищу, это английские предложения, которые я могу молча произносить, глядя на строку кода. Если у вас есть ссылки, которые конкретно касаются именно этого, прекрасно, но я также не ищу ответы, в которых говорится, что я должен пойти и прочесть некоторые основные учебники. Я сделал это, и я получил (по крайней мере, значительное количество) логику. Также обратите внимание, что мне не нужны исчерпывающие ответы (хотя такие попытки приветствуются): приветствуются даже короткие ответы, которые обеспечивают элегантный способ чтения одной конкретной строки кода, который в противном случае был бы проблематичным.

Я предполагаю, что часть этого вопроса такова: могу ли я читать функциональный код линейно, знаете ли, слева направо и сверху вниз? Или кто-то в значительной степени вынужден создавать мысленную картину спагетти-подобной проводки на странице кода, которая определенно не линейна? И если это нужно сделать, нам все равно нужно прочитать код, так как же взять линейный текст и связать спагетти?

Любые советы будут оценены.

const obj1 = { a:1, b:2, c:3, d:3 };
const obj2 = { a:1, b:1, e:2, f:2, g:3, h:5 };

// x or X is key or value or key/value pair

const getXs = (obj, getX) =>
  Object.keys(obj).map(key => getX(obj)(key));

const getPctSameXs = (getX, filter = vals => vals) =>
  (objA, objB) =>
    filter(getXs(objB, getX))
      .reduce(
        (numSame, x) =>
          getXs(objA, getX).indexOf(x) > -1 ? numSame + 1 : numSame,
        0
      ) / Object.keys(objA).length * 100;

const pctSameKeys       = getPctSameXs(obj => key => key);
const pctSameValsDups   = getPctSameXs(obj => key => obj[key]);
const pctSameValsNoDups = getPctSameXs(obj => key => obj[key], vals => [...new Set(vals)]);
const pctSameProps      = getPctSameXs(obj => key => JSON.stringify( {[key]: obj[key]} ));

console.log('obj1:', JSON.stringify(obj1));
console.log('obj2:', JSON.stringify(obj2));
console.log('% same keys:                   ', pctSameKeys      (obj1, obj2));
console.log('% same values, incl duplicates:', pctSameValsDups  (obj1, obj2));
console.log('% same values, no duplicates:  ', pctSameValsNoDups(obj1, obj2));
console.log('% same properties (k/v pairs): ', pctSameProps     (obj1, obj2));

// output:
// obj1: {"a":1,"b":2,"c":3,"d":3}
// obj2: {"a":1,"b":1,"e":2,"f":2,"g":3,"h":5}
// % same keys:                    50
// % same values, incl duplicates: 125
// % same values, no duplicates:   75
// % same properties (k/v pairs):  25
Эндрю Виллемс
источник

Ответы:

18

В основном вам трудно читать, потому что этот конкретный пример не очень читабелен. Без обид, не удручающе большая доля образцов, которые вы находите в Интернете, также не являются. Многие люди по выходным играют только с функциональным программированием, и им никогда не приходится иметь дело с поддержанием производственного функционального кода в долгосрочной перспективе. Я бы написал это так:

function mapObj(obj, f) {
  return Object.keys(obj).map(key => f(obj, key));
}

function getPctSameXs(obj1, obj2, f) {
  const mapped1 = mapObj(obj1, f);
  const mapped2 = mapObj(obj2, f);
  const same = mapped1.filter(x => mapped2.indexOf(x) != -1);
  const percent = same.length / mapped1.length * 100;
  return percent;
}

const getValues = (obj, key) => obj[key];
const valuesWithDupsPercent = getPctSameXs(obj1, obj2, getValues);

По какой-то причине у многих людей возникает мысль, что функциональный код должен иметь определенный эстетический «вид» большого вложенного выражения. Обратите внимание, что хотя моя версия чем-то напоминает императивный код со всеми точками с запятой, все является неизменным, поэтому вы можете заменить все переменные и получить одно большое выражение, если хотите. Это действительно так же «функционально», как версия для спагетти, но с большей читабельностью.

Здесь выражения разбиты на очень маленькие кусочки и даны имена, которые имеют значение для домена. Вложенности можно избежать, если использовать обычные функции, как mapObjв именованной функции. Лямбды зарезервированы для очень коротких функций с четкой целью в контексте.

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

Карл Билефельдт
источник
Определенно без обид! Хотя я все еще буду утверждать, что знаю кое- что о функциональном программировании, возможно, мои утверждения в вопросе о том, насколько я знаю, были немного переоценены. Я действительно относительный новичок. То, что моя конкретная попытка может быть переписана таким кратким, ясным, но все же функциональным способом, похоже на золото ... спасибо. Я буду внимательно изучать вашу переписать.
Эндрю Виллемс
1
Я слышал, что длинные цепочки и / или вложение методов исключают ненужные промежуточные переменные. Напротив, ваш ответ разбивает мои цепочки / вложенность на промежуточные автономные операторы, использующие промежуточные переменные с хорошими именами. Я нахожу ваш код более читабельным в этом случае, но мне интересно, насколько вы вообще пытаетесь быть. Вы говорите, что длинные цепочки методов и / или глубокое вложение часто или даже всегда являются антишаблоном, которого следует избегать, или есть моменты, когда они приносят значительную пользу? И отличается ли ответ на этот вопрос для функционального и императивного кодирования?
Эндрю Виллемс
3
Существуют определенные ситуации, в которых устранение промежуточных переменных может добавить ясности. Например, в FP вы почти никогда не хотите индексировать в массив. Также иногда нет подходящего названия для промежуточного результата. Однако, по моему опыту, большинство людей склонны ошибаться в обратном направлении.
Карл Билефельдт
6

Я не проделал много высокофункциональной работы в Javascript (я бы сказал, что это так - большинство людей, говорящих о функциональном Javascript, могут использовать карты, фильтры и сокращения, но ваш код определяет свои собственные функции более высокого уровня , несколько более продвинутый, чем это), но я сделал это в Haskell, и я думаю, что, по крайней мере, часть опыта переводится. Я дам вам несколько советов о том, что я узнал:

Указание типов функций действительно важно. Haskell не требует, чтобы вы указывали тип функции, но включение типа в определение значительно облегчает чтение. Хотя Javascript не поддерживает явную типизацию таким же образом, нет причин не включать определение типа в комментарий, например:

// getXs :: forall O, F . O -> (O -> String -> F) -> [F]
const getXs = (obj, getX) =>
    Object.keys(obj).map(key => getX(obj)(key));

Немного попрактиковавшись в работе с такими определениями типов, они делают смысл функции намного более понятным.

Именование важно, возможно, даже больше, чем в процедурном программировании. Многие функциональные программы написаны в очень лаконичном стиле, сложном для соглашения (например, соглашение о том, что «xs» является списком / массивом, а «x» является элементом в нем, очень распространено), но если вы не понимаете этот стиль легко я бы предложил более подробное наименование. Глядя на конкретные имена, которые вы использовали, «getX» вроде бы непрозрачен, и поэтому «getXs» тоже не очень помогает. Я бы назвал «getXs» чем-то вроде «applyToProperties», а «getX», вероятно, будет «propertyMapper». «getPctSameXs» будет тогда «процентPropertiesSameWith» («с»).

Другой важной вещью является написание идиоматического кода . Я заметил, что вы используете синтаксис a => b => some-expression-involving-a-and-bдля создания функций карри. Это интересно и может быть полезно в некоторых ситуациях, но вы не делаете здесь ничего, что выигрывает от карри-функций, и было бы более идиоматичным Javascript вместо этого использовать традиционные функции с несколькими аргументами. Это может помочь с первого взгляда увидеть, что происходит. Вы также используете const name = lambda-expressionдля определения функций, где function name (args) { ... }вместо этого было бы более идиоматичным . Я знаю, что они семантически немного отличаются, но если вы не полагаетесь на эти различия, я бы предложил использовать более общий вариант, когда это возможно.

Жюль
источник
5
+1 для типов! То, что в языке их нет, не означает, что вам не нужно думать о них . Несколько систем документации для ECMAScript имеют язык типов для записи типов функций. Некоторые IDE ECMAScript также имеют язык типов (и, как правило, они также понимают языки типов для основных систем документации), и они могут даже выполнять элементарную проверку типов и эвристический хинтинг, используя эти аннотации типов .
Jörg W Mittag
Вы дали мне много разжевывать: определения типов, значимые имена, использование идиом ... спасибо! Лишь несколько возможных комментариев: я не собирался писать определенные части как функции с карри; они просто развивались таким образом, когда я реорганизовывал свой код во время написания. Теперь я вижу, как это не нужно, и даже просто объединение параметров этих двух функций в два параметра для одной функции не только имеет больше смысла, но и мгновенно делает этот короткий бит по меньшей мере более читабельным.
Эндрю Виллемс
@ JörgWMittag, спасибо за ваши комментарии о важности типов и за ссылку на другой ответ, который вы написали. Я использую WebStorm и не осознавал, что согласно тому, как я прочитал ваш другой ответ, WebStorm знает, как интерпретировать jsdoc-подобные аннотации. Из вашего комментария я предполагаю, что jsdoc и WebStorm могут использоваться вместе для аннотирования функционального, а не просто императивного кода, но мне нужно углубиться, чтобы действительно знать это. Я играл с jsdoc раньше и теперь, когда я знаю, что WebStorm и я могу там сотрудничать, я ожидаю, что буду использовать эту функцию / подход больше.
Эндрю Виллемс
@Jules, просто чтобы уточнить, на какую функцию карри я ссылался в моем комментарии выше: Как вы и предполагали, каждый экземпляр obj => key => ...можно упростить, (obj, key) => ...потому что позже getX(obj)(key)можно упростить и его get(obj, key). Напротив, другая функция с карри, (getX, filter = vals => vals) => (objA, objB) => ...не может быть легко упрощена, по крайней мере, в контексте остальной части кода, как написано.
Эндрю Виллемс