Як я можу _read_ функціональний код JavaScript?


9

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

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

  • відсоток ключів у 1-му об'єкті, який можна знайти у другому,
  • відсоток значень першого об'єкта, який можна знайти у другому, включаючи дублікати,
  • відсоток значень у 1-му об'єкті, який можна знайти у 2-му, без дублікатів, і
  • відсотків пар {key: value} пар у 1-му об'єкті, який можна знайти у 2-му об’єкті.

Я почав з досить імперативного коду, але швидко зрозумів, що це проблема, яка добре підходить для функціонального програмування. Зокрема, я зрозумів, що якщо я можу витягнути функцію або три для кожної з перерахованих вище чотирьох стратегій, які визначали тип функції, яку я прагну порівняти (наприклад, ключі або значення тощо), то я можу бути в змозі скоротити (помилуйте гру на словах) решту коду на повторювані одиниці. Ви знаєте, зберігаючи це СУХО. Тому я перейшов на функціональне програмування. Я дуже пишаюся результатом, думаю, що це досить елегантно, і я думаю, що розумію, що зробив досить добре.

Однак, навіть написавши код сам і розуміючи кожну його частину під час будівництва, коли я зараз оглядаюсь на нього, я продовжую бути трохи збентежений як читати будь-який конкретний пів рядка, так і як "grok", що насправді робить будь-який конкретний піврядковий код. Мені здається, що я створюю психічні стрілки для з'єднання різних частин, які швидко розпадаються на безлад спагетті.

Отже, чи може хто-небудь підказати мені, як "прочитати" деякі більш складні біти коду таким чином, щоб бути і стислим, і це сприяє моєму розумінню того, що я читаю? Я здогадуюсь, що частин, які мене найбільше отримують, - це ті, у яких кілька жирових стрілок поспіль та / або частини, які мають кілька дужок підряд. Знову ж таки, в їх основі, я можу врешті-решт з'ясувати логіку, але (сподіваюсь), є кращий спосіб швидко та зрозуміло, і безпосередньо "перейняти" ряд функціональних програм програмування 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 мають мову типу для запису типів функцій. Кілька ECMAScript IDE також мають мову типу (і зазвичай вони також розуміють мови типів для основних систем документації), і вони можуть навіть виконувати рудиментарну перевірку типу та евристичне натякнення, використовуючи ці типові анотації .
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) => ...не може бути легко спрощена, принаймні в контексті решти коду, як написано.
Ендрю Віллемс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.