Как справиться с грязными побочными эффектами с помощью чистого функционального JavaScript

внешний интерфейс JavaScript Программа перевода самородков TensorFlow

Как справиться с грязными побочными эффектами с помощью чистого функционального JavaScript

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

Чистая функция — это функция без побочных эффектов.[1]Но если вы понимаете программирование, вы узнаете, что побочные эффектыЖизненноважный. Если значение 𝜋 невозможно прочитать, почему оно вычисляется во многих местах? Чтобы напечатать значение, нам нужно написать оператор консоли, отправить его на принтер или что-то еще, что можно прочитать.место. Если база данных не может ввести никаких данных, то какой в ​​ней смысл? Нам нужно считывать данные с устройств ввода и запрашивать информацию по сети. Ни одна из этих вещей не может быть без побочных эффектов. Однако функциональное программирование построено на чистых функциях. Так как же функциональные программисты добиваются цели?

Проще говоря, делайте то, что делают математики: жульничайте.

Скажем, они жульничают и технически следуют правилам. Но они нашли лазейки в этих правилах и воспользовались ими. Существует два основных метода:

  1. внедрение зависимости, или мы можем назвать этопроблема на удержании
  2. Использование функторов эффектов, мы можем представить это какПрокрастинация[2]

внедрение зависимости

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

// logSomething :: String -> ()
function logSomething(something) {
  const dt = new Date().toIsoString();
  console.log(`${dt}: ${something}`);
  return something;
}

logSomething()Функция нечиста по двум причинам: она создаетDate()объект и вывести его на консоль. Таким образом, он не только выполняет операции ввода-вывода, но и дает разные результаты при каждом запуске. Итак, как сделать эту функцию чистой? При внедрении зависимостей мы принимаем нечистые части в виде параметров функции, поэтомуlogSomething()Функция принимает три аргумента вместо одного:

// logSomething: Date -> Console -> String -> ()
function logSomething(d, cnsl, something) {
  const dt = d.toIsoString();
  cnsl.log(`${dt}: ${something}`);
  return something;
}

Затем, чтобы вызвать его, мы должны сами явно передать нечистую часть:

const something = "Curiouser and curiouser!";
const d = new Date();
logSomething(d, console, something);
// ⦘ Curiouser and curiouser!

Теперь вы можете подумать: «Это какая-то глупость. Это усугубляет проблему, а код остается таким же нечистым, как и раньше». Ты прав. Это полная лазейка.

Ссылка на видео на ютубе:youtu.be/9ZSoJDUD_bU

Это как придуриваться: "О! Нет! Офицер, я не знаю, гдеcnslзвонитьlog()Операции ввода-вывода будут выполнены. Это было передано мне кем-то другим. Я не знаю, откуда это взялось», что выглядит довольно глупо.

Это не так глупо, как кажется, обратите внимание на нашуlogSomething()функция. Если вы собираетесь иметь дело с чем-то нечистым, вы должны положить этосталиНечистый. Мы можем просто передать разные параметры:

const d = {toISOString: () => "1865-11-26T16:00:00.000Z"};
const cnsl = {
  log: () => {
    // do nothing
  }
};
logSomething(d, cnsl, "Off with their heads!");
//  ← "Off with their heads!"

Прямо сейчас наша функция ничего не делает, кроме возвратаsomethingпараметр.但是它是纯的。如果你用相同的参数调用它,它每次都会返回相同的结果。 В этом-то и дело.为了使它变得不纯,我们必须采取深思熟虑的行动。或者换句话说,函数依赖于右边的签名。函数无法访问到像consoleилиDateтакие как глобальные переменные. Вот так все ясно.

Также обратите внимание, что мы также можем передавать функции изначально нечистым функциям. Давайте посмотрим на другой пример. Предположим, что форма имеетusernameполе. Мы хотим получить его значение из формы:

// getUserNameFromDOM :: () -> String
function getUserNameFromDOM() {
  return document.querySelector("#username").value;
}

const username = getUserNameFromDOM();
username;
// ← "mhatter"

В этом примере мы пытаемся запросить информацию из DOM. Это нечисто, потому чтоdocumentГлобальная переменная может измениться в любое время. Один из способов нашей функции в чистой функции является глобальнойdocumentОбъект передается в качестве параметра. Но мы также можем пройти черезquerySelector()функция:

// getUserNameFromDOM :: (String -> Element) -> String
function getUserNameFromDOM($) {
  return $("#username").value;
}

// qs :: String -> Element
const qs = document.querySelector.bind(document);

const username = getUserNameFromDOM(qs);
username;
// ← "mhatter"

Возможно, вы все еще думаете: «Это все так же глупо!»getUsernameFromDOM()Просто съехать. Он не ушел, мы просто поместили его в другую функциюqs()середина. Кажется, это мало что делает, кроме как делает код длиннее. Две наши функции заменяют предыдущую нечистую функцию, но одна из них все еще нечистая.

Не волнуйтесь, предположим, мы хотим датьgetUserNameFromDOM()Пишите тесты. Теперь сравните нечистую и чистую версии, на какой проще писать тесты? Чтобы протестировать нечистые версии функций, нам нужен глобальныйdocumentобъект, кроме того, требует IDusernameЭлементы. Если я хочу протестировать его вне браузера, мне нужно импортировать что-то вроде JSDOM или безголового браузера. Все дело в тестировании крошечной функции. Но со второй версией функции я могу сделать так:

const qsStub = () => ({value: "mhatter"});
const username = getUserNameFromDOM(qsStub);
assert.strictEqual("mhatter", username, `Expected username to be ${username}`);

Это не означает, что вы не должны создавать интеграционные тесты, которые запускаются в реальных браузерах. (Или, по крайней мере, издевательская версия, такая как JSDOM). Но то, что показывает этот пример,getUserNameFromDOM()Теперь это абсолютно предсказуемо. Если мы передаем его в qsStub, он всегда будет возвращатьсяmhatter. Мы перенесли непредсказуемость в меньшую функцию qs.

Если мы это сделаем, мы сможем продвигать эту непредсказуемость все дальше и дальше. В конечном счете, мы выталкиваем их за пределы кода. Таким образом, мы получаем тонкую оболочку нечистого кода, окружающую удобное для тестирования предсказуемое ядро. Эта предсказуемость вступает в игру, когда вы начинаете создавать более крупные приложения.

Недостатки внедрения зависимостей

可以以这种方式创建大型、复杂的应用程序。 Я знаю, этопотому что я сделал. Внедрение зависимостей упрощает тестирование, а также делает явными зависимости каждой функции. Но он также имеет некоторые недостатки. Суть в том, что в итоге вы получите подробные сигнатуры функций, подобные этой:

function app(doc, con, ftch, store, config, ga, d, random) {
  // 这里是应用程序代码
}

app(document, console, fetch, store, config, ga, new Date(), Math.random);

Это не так уж плохо, за исключением того, что у вас могут возникнуть проблемы с параметрическим бурением. В низкоуровневой функции вам может понадобиться один из этих параметров. Таким образом, вам нужно объединить параметры через множество уровней вызовов функций. Это раздражает. Например, вам может понадобиться передать даты через 5 уровней промежуточных функций. Все эти промежуточные функции не используют объекты даты. Это не конец света, по крайней мере приятно видеть эти явные зависимости. Но это все еще раздражает. Вот еще способ...

ленивая функция

Давайте посмотрим на вторую уязвимость, которую используют функциональные программисты. Это звучит примерно так: «Происходящие побочные эффекты — это побочные эффекты». Я знаю, это звучит загадочно. Давайте попробуем сделать это немного более явным. Рассмотрим этот код:

// fZero :: () -> Number
function fZero() {
  console.log("Launching nuclear missiles");
  // 这里是发射核弹的代码
  return 0;
}

Я знаю, что это глупый пример. Если бы мы хотели, чтобы в коде был 0, мы могли бы просто выписать его. Я знаю, что ты, элегантный читатель, никогда не напишешь код для управления ядерным оружием на JavaScript. Но это помогает проиллюстрировать суть. Это явно нечистый код. Так как он выводит логи на консоль, то может и начать термоядерную войну. Предположим, мы хотим 0. Предположим, мы хотим рассчитать, что происходит после запуска ракеты, нам может понадобиться запустить что-то вроде обратного отсчета. В этом случае имеет смысл заранее спланировать, как будут выполняться расчеты. Мы очень осторожны, когда эти ракеты взлетают, и мы не хотим испортить наши расчеты, чтобы они случайно не запустили ракеты. Тогда, если мы будемfZero()Как насчет того, чтобы обернуть другую функцию, которая просто возвращает его? Что-то вроде защитной пленки.

// fZero :: () -> Number
function fZero() {
  console.log("Launching nuclear missiles");
  // 这里是发射核弹的代码
  return 0;
}

// returnZeroFunc :: () -> (() -> Number)
function returnZeroFunc() {
  return fZero;
}

я могу бежатьreturnZeroFunc()Любое количество раз, пока не вызывается возвращаемое значение, теоретически я в безопасности. Мой код не стреляет ядерными бомбами.

const zeroFunc1 = returnZeroFunc();
const zeroFunc2 = returnZeroFunc();
const zeroFunc3 = returnZeroFunc();
// 没有发射核弹。

Теперь давайте определим чистые функции более формально. Затем мы можем изучить нашуreturnZeroFunc()функция. Функция называется чистой, если она удовлетворяет следующим условиям:

  1. Нет очевидных побочных эффектов
  2. Прозрачность котировок. То есть при одном и том же входе он всегда возвращает один и тот же результат.

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

returnZeroFunc()Являются ли цитаты прозрачными? То есть при одном и том же входе он всегда возвращает один и тот же результат? Ну, как сейчас написано, мы можем протестировать это:

zeroFunc1 === zeroFunc2; // true
zeroFunc2 === zeroFunc3; // true

Но это еще не чисто.returnZeroFunc()Функция ссылается на переменную вне области действия функции. Чтобы исправить это, мы можем переписать его следующим образом:

// returnZeroFunc :: () -> (() -> Number)
function returnZeroFunc() {
  function fZero() {
    console.log("Launching nuclear missiles");
    // 这里是发射核弹的代码
    return 0;
  }
  return fZero;
}

Теперь наша функция чистая. Однако нас сдерживает JavaScript. мы больше не можем использовать===для проверки ссылочной прозрачности. Это потому чтоreturnZeroFunc()Всегда возвращайте новую ссылку на функцию. Но вы можете проверить ссылочную прозрачность, просмотрев код.returnZeroFunc()Функция ничего не делает, но каждый раз возвращает одну и ту же функцию.

Это аккуратная маленькая лазейка. Но можем ли мы действительно использовать его для реального кода? Ответ положительный. Но прежде чем мы обсудим, как реализовать это на практике, отложим это в сторону. вернуться к опаснымfZero()функция:

// fZero :: () -> Number
function fZero() {
  console.log("Launching nuclear missiles");
  // 这里是发射核弹的代码
  return 0;
}

попробуем использоватьfZero()Вернул ноль, но это не начало бы термоядерной войны (смеется). Мы создадим функцию, которая принимаетfZero()Окончательный возврат равен 0, и к нему добавляется единица:

// fIncrement :: (() -> Number) -> Number
function fIncrement(f) {
  return f() + 1;
}

fIncrement(fZero);
// ⦘ 发射导弹
// ← 1

Упс! Мы случайно начали термоядерную войну. Давай еще раз попробуем. На этот раз мы не будем возвращать число. Вместо этого мы вернем функцию, которая в итоге вернет число:

// fIncrement :: (() -> Number) -> (() -> Number)
function fIncrement(f) {
  return () => f() + 1;
}

fIncrement(zero);
// ← [Function]

Эй! Кризис предотвращен. давай продолжим. С помощью этих двух функций мы можем создать серию «окончательных чисел»:

const fOne = fIncrement(zero);
const fTwo = fIncrement(one);
const fThree = fIncrement(two);
// 等等…

Мы также можем создать наборf*()функция для обработки конечного значения:

// fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number)
function fMultiply(a, b) {
  return () => a() * b();
}

// fPow :: (() -> Number) -> (() -> Number) -> (() -> Number)
function fPow(a, b) {
  return () => Math.pow(a(), b());
}

// fSqrt :: (() -> Number) -> (() -> Number)
function fSqrt(x) {
  return () => Math.sqrt(x());
}

const fFour = fPow(fTwo, fTwo);
const fEight = fMultiply(fFour, fTwo);
const fTwentySeven = fPow(fThree, fThree);
const fNine = fSqrt(fTwentySeven);
// 没有控制台日志或热核战争。干得不错!

Видишь, что мы сделали? Если это можно сделать с обычными числами, то можно использовать и окончательные числа. математика называет этоизоморфизм. Мы всегда можем поместить обычное число в функцию, чтобы превратить его в окончательное число. Мы можем получить окончательное число, вызвав эту функцию. Другими словами, мы строим отображение между числом и конечным числом. Это более захватывающе, чем кажется. Обещаю, мы скоро к этому вернемся.

Этот функциональный пакет является правовой стратегией. Мы можем скрывать за функцией, как долго это займет давно? Пока мы не называем эти функции, они теоретически чистые. Мир во всем мире. В рутинном (не ядерном) коде мы на самом деле, наконец, надеемся, что эти побочные эффекты могут работать. Упакуйте все вещи в функции, позволяет нам точно контролировать эти эффекты. Мы решаем точное время этих побочных эффектов. Однако ввод этих скобок очень больно. Новая версия каждой функции раздражает. У нас есть очень хорошие функции на языке, напримерMath.sqrt(). Было бы неплохо, если бы был способ использовать эти обычные функции с отложенными значениями. Перейдите к следующему разделу, Функторам эффектов.

Функтор эффекта

Для целей функтор Effect — это не что иное, как объект, помещенный в отложенную функцию. мы хотим поставитьfZeroФункция помещается в объект Effect. Однако перед этим уменьшите сложность на один уровень.

// zero :: () -> Number
function fZero() {
  console.log("Starting with nothing");
  // 绝对不会在这里发动核打击。
  // 但是这个函数仍然不纯
  return 0;
}

Теперь мы создаем конструктор, который возвращает объект Effect.

// Effect :: Function -> Effect
function Effect(f) {
  return {};
}

Пока ничего не видно. Давайте сделаем что-нибудь полезное. Мы хотим использовать обычныйfZero()функция. Мы напишем метод, который принимает обычную функцию и возвращает отложенное значение, которое выполняется без каких-либо эффектов. мы называем егоmap. Это связано с тем, что он создаеткарта. Это может выглядеть так:

// Effect :: Function -> Effect
function Effect(f) {
  return {
    map(g) {
      return Effect(x => g(f(x)));
    }
  };
}

Теперь, если вы посмотрите внимательно, вы можете удивитьсяmap()эффект. Похоже на комбинацию. Мы вернемся к этому позже. Теперь давайте попробуем:

const zero = Effect(fZero);
const increment = x => x + 1; // 一个普通的函数。
const one = zero.map(increment);

Эм. Мы не видели, что произошло. Давайте изменим Эффект, чтобы у нас был способ «нажать на курок». Это можно написать так:

// Effect :: Function -> Effect
function Effect(f) {
  return {
    map(g) {
      return Effect(x => g(f(x)));
    },
    runEffects(x) {
      return f(x);
    }
  };
}

const zero = Effect(fZero);
const increment = x => x + 1; // 只是一个普通的函数
const one = zero.map(increment);

one.runEffects();
// ⦘ 什么也没启动
// ← 1

и мы можем звонить сколько угодноmapфункция:

const double = x => x * 2;
const cube = x => Math.pow(x, 3);
const eight = Effect(fZero)
  .map(increment)
  .map(double)
  .map(cube);

eight.runEffects();
// ⦘ 什么也没启动
// ← 8

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

Если у нас есть Эффектe, две функцииfа такжеg
Такe.map(g).map(f)Эквивалентноe.map(x => f(g(x))).

Другими словами, напишите два в строкеmapФункция эквивалентна объединению двух функций. То есть Эффект можно записать так (вспомним пример выше):

const incDoubleCube = x => cube(double(increment(x)));
// 如果你使用像 Ramda 或者 lodash/fp 之类的库,我们也可以这样写:
// const incDoubleCube = compose(cube, double, increment);
const eight = Effect(fZero).map(incDoubleCube);

Когда мы это сделаем, мы можем подтвердить, что получимmapверсия с тем же результатом. Мы можем использовать его для рефакторинга нашего кода и быть уверенными, что код не сломается. В некоторых случаях мы можем даже улучшить производительность, переключаясь между разными методами.

Но этих примеров достаточно, приступим.

Сокращение эффекта

Наш конструктор Effect принимает функцию в качестве аргумента. Это удобно, потому что большинство побочных эффектов, которые мы хотим отсрочить, также являются функциями. Например,Math.random()а такжеconsole.log()Это все такие вещи. Но иногда мы хотим сжать простое старое значение в Эффект. Например, предположим, что мы находимся в браузереwindowНекоторые объекты конфигурации присоединены к глобальному объекту. Мы хотим получить значение A, но это не чистая операция. Мы можем написать небольшое сокращение, которое облегчит эту задачу:[3]

// of :: a -> Effect a
Effect.of = function of(val) {
  return Effect(() => val);
};

Чтобы проиллюстрировать, насколько это может быть удобно, предположим, что мы имеем дело с веб-приложением. Приложение имеет некоторые стандартные функции, такие как список статей и профили пользователей. Но в HTML эти компоненты отображаются для разных клиентов. Поскольку мы умные инженеры, мы решили сохранить их местоположение в глобальном объекте конфигурации, чтобы мы всегда могли их найти. Например:

window.myAppConf = {
  selectors: {
    "user-bio": ".userbio",
    "article-list": "#articles",
    "user-name": ".userfullname"
  },
  templates: {
    greet: "Pleased to meet you, {name}",
    notify: "You have {n} alerts"
  }
};

используй сейчасEffect.of(), мы можем быстро обернуть нужные нам значения в контейнер Effect, вот так

const win = Effect.of(window);
userBioLocator = win.map(x => x.myAppConf.selectors["user-bio"]);
// ← Effect('.userbio')

Встроенные и не встроенные эффекты

Картографические эффекты могут нам очень помочь. Но иногда мы сталкиваемся с ситуациями, когда отображаемая функция также возвращает эффект. Мы определилиgetElementLocator(), который возвращает эффект, содержащий строку. Если мы действительно хотим получить элемент DOM, нам нужно вызвать другую нечистую функцию.document.querySelector(). Таким образом, мы могли бы очистить его, вернув эффект:

// $ :: String -> Effect DOMElement
function $(selector) {
  return Effect.of(document.querySelector(s));
}

Теперь, если мы хотим соединить их вместе, мы можем попробовать использоватьmap():

const userBio = userBioLocator.map($);
// ← Effect(Effect(<div>))

Это немного неудобно, чтобы заставить его работать. Если мы хотим получить доступ к этому div, нам нужно сопоставить то, что мы хотим сделать с функцией. Например, если мы хотим получитьinnerHTML, это выглядит так:

const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML));
// ← Effect(Effect('<h2>User Biography</h2>'))

Попробуем разобрать. мы вернемсяuserBio, затем продолжите. Это немного утомительно, но мы хотим выяснить, что здесь происходит. Теги, которые мы используемEffect('user-bio')Какое-то заблуждение. Если мы напишем это как код, это будет выглядеть примерно так:

Effect(() => ".userbio");

Но это тоже не точно. Что мы делаем на самом деле:

Effect(() => window.myAppConf.selectors["user-bio"]);

Теперь, когда мы делаем отображение, это эквивалентно составлению внутренней функции с другой функцией (как мы видели выше). Итак, когда мы используем$При отображении это выглядит так:

Effect(() => window.myAppConf.selectors["user-bio"]);

Разверните его, чтобы получить:

Effect(
  () => Effect.of(document.querySelector(window.myAppConf.selectors['user-bio'])))
);

расширятьEffect.ofДайте нам более четкое представление:

Effect(() =>
  Effect(() => document.querySelector(window.myAppConf.selectors["user-bio"]))
);

Примечание. Весь код, который фактически выполняет действие, находится в самой внутренней функции, ничего из этого не просачивается во внешний эффект.

Join

Почему так пишется? Мы хотим, чтобы эти встроенные эффекты были невстроенными. В процессе преобразования убедитесь, что не возникают непреднамеренные побочные эффекты. Для Эффекта способ не встраивать его состоит в том, чтобы вызвать внешнюю функцию.runEffects(). Но это может сбивать с толку. Мы выполнили все упражнение, чтобы убедиться, что мы не будем запускать никаких эффектов. Мы создадим еще одну функцию, которая делает то же самое, и назовем ееjoin. Мы используемjoinЧтобы решить проблему встраивания Эффекта, используйтеrunEffects()Действительно запустить все эффекты. Несмотря на то, что код для запуска тот же, это сделает наши намерения более ясными.

// Effect :: Function -> Effect
function Effect(f) {
  return {
    map(g) {
        return Effect(x => g(f(x)));
    },
    runEffects(x) {
        return f(x);
    }
    join(x) {
        return f(x);
    }
  }
}

Затем его можно использовать для распаковки встроенного элемента профиля пользователя:

const userBioHTML = Effect.of(window)
  .map(x => x.myAppConf.selectors["user-bio"])
  .map($)
  .join()
  .map(x => x.innerHTML);
// ← Effect('<h2>User Biography</h2>')

Chain

.map()с последующим.join()Эта закономерность встречается часто. На самом деле удобно иметь сокращенную функцию. Таким образом, всякий раз, когда у нас есть функция, возвращающая эффект, мы можем использовать эту сокращенную функцию. Это может отвлечь нас от написания снова и сноваmapзатем следуйтеjoinспасен. Мы пишем это:

// Effect :: Function -> Effect
function Effect(f) {
    return {
        map(g) {
            return Effect(x => g(f(x)));
        },
        runEffects(x) {
            return f(x);
        }
        join(x) {
            return f(x);
        }
        chain(g) {
            return Effect(f).map(g).join();
        }
    }
}

мы называем новую функциюchain()Потому что это позволяет нам соединять Эффекты вместе. (На самом деле также потому, что стандарт говорит нам называть это так).[4]Получить элемент профиля пользователяinnerHTMLЭто может выглядеть так:

const userBioHTML = Effect.of(window)
  .map(x => x.myAppConf.selectors["user-bio"])
  .chain($)
  .map(x => x.innerHTML);
// ← Effect('<h2>User Biography</h2>')

К сожалению, другие функциональные языки имеют несколько иные названия для этой реализации. Если вы читаете это, вы можете быть немного сбиты с толку. иногда это называетсяflatMap, имя имеет смысл, потому что мы сначала делаем сопоставление нормалей, а затем используем.join()Сгладить результат. Но в Хаскелеchainбыло дано запутанное имяbind. Так что, если вы читали это в другом месте, помнитеchain,flatMapа такжеbindСобственно отсылка к одному и тому же понятию.

Объединить эффекты

Это последний сценарий, в котором использование эффекта немного неудобно, и мы хотим объединить два или более функтора в одну функцию. Например, как мне получить имя пользователя из DOM? Нужно ли после получения имени вставлять его в шаблон, предоставленный конфигурацией приложения? Итак, у нас может быть шаблонная функция (обратите внимание, что мы создадим каррированную версию функции)

// tpl :: String -> Object -> String
const tpl = curry(function tpl(pattern, data) {
    return Object.keys(data).reduce(
        (str, key) => str.replace(new RegExp(`{${key}}`, data[key]),
        pattern
    );
});

Все хорошо, но теперь для получения данных нам нужно:

const win = Effect.of(window);
const name = win.map(w => w.myAppConfig.selectors['user-name'])
    .chain($)
    .map(el => el.innerHTML)
    .map(str => ({name: str});
// ← Effect({name: 'Mr. Hatter'});

const pattern = win.map(w => w.myAppConfig.templates('greeting'));
// ← Effect('Pleased to meet you, {name}');

У нас уже есть шаблонная функция. Он принимает строку и объект и возвращает строку. Но наши строки и объекты (nameа такжеpattern) был упакован в эффект. Все, что нам нужно сделать, это поднять насtpl()Функции на более высоком месте позволяют хорошо работать с эффектом.

Давайте посмотрим, используем ли мы шаблон Эффектmap()передачаtpl()Что случится:

pattern.map(tpl);
// ← Effect([Function])

Взгляд на типы может прояснить ситуацию. Объявление функции для карты может выглядеть так:

_map :: Effect a ~> (a -> b) -> Effect b_

Вот объявление функции для функции шаблона:

_tpl :: String -> Object -> String_

Поэтому, когда мыpatternзвонитьmap, мы получаемчастичное применениефункция (помните, мы карриtpl).

_Effect (Object -> String)_

Теперь мы хотим передать значения изнутри паттерна Effect, но у нас пока нет способа сделать это. Мы напишем еще один метод Effect (называемыйap()), чтобы справиться с этим:

// Effect :: Function -> Effect
function Effect(f) {
    return {
        map(g) {
            return Effect(x => g(f(x)));
        },
        runEffects(x) {
            return f(x);
        }
        join(x) {
            return f(x);
        }
        chain(g) {
            return Effect(f).map(g).join();
        }
        ap(eff) {
            //  如果有人调用了 ap,我们假定 eff 里面有一个函数而不是一个值。
            // 我们将用 map 来进入 eff 内部, 并且访问那个函数
            // 拿到 g 后,就传入 f() 的返回值
            return eff.map(g => g(f()));
        }
    }
}

С ним мы можем бежать.ap()Чтобы применить нашу функцию шаблона:

const win = Effect.of(window);
const name = win
  .map(w => w.myAppConfig.selectors["user-name"])
  .chain($)
  .map(el => el.innerHTML)
  .map(str => ({ name: str }));

const pattern = win.map(w => w.myAppConfig.templates("greeting"));

const greeting = name.ap(pattern.map(tpl));
// ← Effect('Pleased to meet you, Mr Hatter')

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

// liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c)
const liftA2 = curry(function liftA2(f, x, y) {
  return y.ap(x.map(f));
  // 我们也可以这样写:
  //  return x.map(f).chain(g => y.map(g));
});

мы называем егоliftA2()потому что он поднимает функцию, которая принимает два аргумента Мы можем написать аналогичныйliftA3(),так:

// liftA3 :: (a -> b -> c -> d) -> (Applicative a -> Applicative b -> Applicative c -> Applicative d)
const liftA3 = curry(function liftA3(f, a, b, c) {
  return c.ap(b.ap(a.map(f)));
});

Уведомление,liftA2а такжеliftA3Эффект никогда не упоминается. Теоретически они могут быть совместимы с любымap()Объекты методов работают вместе. использоватьliftA2()Мы можем переписать предыдущий пример следующим образом:

const win = Effect.of(window);
const user = win.map(w => w.myAppConfig.selectors['user-name'])
    .chain($)
    .map(el => el.innerHTML)
    .map(str => ({name: str});

const pattern = win.map(w => w.myAppConfig.templates['greeting']);

const greeting = liftA2(tpl)(pattern, user);
// ← Effect('Pleased to meet you, Mr Hatter')

И что?

这时候你可能会想:“这似乎为了避免随处可见的奇怪的副作用而付出了很多努力”。 Что это значит?传入参数到 Effect 内部,封装ap()Кажется, тяжелая работа. Зачем беспокоиться, когда нечистый код работает нормально? В реальном сценарии, когда вам это понадобится?

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

— Джон Хьюз[5]

Разобьем эти возражения на два вопроса:

  1. Действительно ли важна функциональная чистота?
  2. Когда это полезно в реальных сценариях?

Функциональная чистота Значение

Функциональная чистота имеет значение. Когда вы наблюдаете небольшую функцию изолированно, небольшой побочный эффект не имеет значения. Напишитеconst pattern = window.myAppConfig.templates['greeting'];Гораздо быстрее и проще, чем писать код, подобный следующему.

const pattern = Effect.of(window).map(w => w.myAppConfig.templates("greeting"));

Если код переполнен такими маленькими функциями, можно продолжать писать так, а побочных эффектов недостаточно, чтобы стать проблемой. Но это всего лишь одна строка кода в приложении, которое может содержать тысячи или даже миллионы строк кода. Функциональная чистота становится еще более важной, когда вы пытаетесь выяснить, почему ваше приложение по необъяснимым причинам перестало работать «по-видимому, без всякой причины». Если происходит что-то неожиданное, вы пытаетесь разобраться в проблеме и выяснить, почему. В этом случае, чем больше кода можно исключить, тем лучше. Если ваши функции чистые, вы можете быть уверены, что единственное, что влияет на их поведение, — это передаваемые им данные. Это значительно сужает диапазон рассматриваемых исключений. Другими словами, это заставляет вас меньше думать. Это особенно важно в больших и сложных приложениях.

Режим эффекта в реальной сцене

Отлично. Если вы создаете большое сложное приложение, такое как Facebook или Gmail. Тогда функциональная чистота может быть важна. Но что, если это не большое приложение? Давайте рассмотрим все более распространенный сценарий. У вас есть некоторые данные. Не просто немного данных, а много данных — миллионы строк в текстовых файлах CSV или больших таблицах базы данных. Ваша задача — обработать эти данные. Возможно, вы обучаете искусственную нейронную сеть построению модели логического вывода. Возможно, вы пытаетесь понять, что будет дальше в криптовалюте. В любом случае, проблема в том, что для выполнения этой работы требуется много обработки.

Джоэл Спольски убедительно доказываетФункциональное программирование может помочь нам решить эту проблему.. Мы можем написать параллельный запускmapа такжеreduceАльтернативная версия и функциональная чистота делают это возможным. Но это не конец истории. Конечно, вы можете написать какой-нибудь причудливый код параллельной обработки. Но даже в этом случае ваша машина для разработки по-прежнему имеет только 4 ядра (или, может быть, 8 или 16, если вам повезет). Эта работа по-прежнему занимает много времени. Если, конечно, вы не сможете запустить его на связке процессоров, вроде графических процессоров, или на целом кластере обрабатывающих серверов.

Чтобы заставить его работать, вам нужно описать вычисление, которое вы хотите запустить. Однако вам нужно описать их без фактического запуска. Звучит знакомо? В идеале вы должны передать описание в какой-то фреймворк. Фреймворк тщательно позаботится о чтении всех данных и разделит их между узлами обработки. Затем фреймворк собирает результаты и сообщает вам, как они работают. Так работает TensorFlow.

TensorFlow™ — это программная библиотека с открытым исходным кодом для высокопроизводительных численных вычислений. Его гибкая архитектура поддерживает кроссплатформенные (CPU, GPU, TPU) развертывания вычислений от настольных компьютеров до кластеров серверов, от мобильных устройств до периферийных устройств. Исследователи и инженеры из группы Google Brain в рамках организации Google AI изначально разработали TensorFlow для поддержки областей машинного и глубокого обучения, а его гибкие вычислительные ядра нашли применение и в других научных областях.

-Тоснований домой[6]

При использовании Tensorflow вы не будете использовать обычный язык программирования данных, который вы используете. Вместо этого вам нужно создать тензор. Если мы хотим добавить два числа, это выглядит так:

node1 = tf.constant(3.0, tf.float32)
node2 = tf.constant(4.0, tf.float32)
node3 = tf.add(node1, node2)

Приведенный выше код написан на Python, но он не сильно отличается от JavaScript, не так ли? Подобно нашему Эффекту,addне запустится, пока мы его не вызовем (в этом примере с использованиемsess.run()):

print("node3: ", node3)
print("sess.run(node3): ", sess.run(node3))
#⦘ node3:  Tensor("Add_2:0", shape=(), dtype=float32)
#⦘ sess.run(node3):  7.0

вызовsess.run()Раньше мы бы не получили 7.0. Как видите, она очень похожа на функцию задержки. Мы планируем наши расчеты заранее. Затем, как только будете готовы, отправляйтесь на войну.

Суммировать

В этой статье есть о чем рассказать, но мы рассмотрели два способа обеспечения функциональной чистоты кода:

  1. внедрение зависимости
  2. Функтор эффекта

Внедрение зависимостей работает путем удаления нечистых частей кода из функций. Поэтому вы должны передать их в качестве параметров. Напротив, функторы Effect работают, обертывая все, что находится за функцией. Чтобы запустить эти эффекты, мы должны сначала запустить функцию-оболочку.

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

  1. Это не полное определение, но пока работает. Мы вернемся к формальному определению позже.

  2. В других языках (таких как Haskell) это называется IOphragm или IO list.PureScriptиспользоватьEffectкак термин. Я нахожу это более описательным.

  3. Обратите внимание, что разные языки имеют разные названия для этого сокращения. Например, в Haskell это называетсяpure. Я не знаю, почему.

  4. В этом примере с помощьюFantasy Land specification for ChainТехнические характеристики.

  5. Джон Хьюз, 1990, «Почему функциональное программирование имеет значение»,Research Topics in Functional Programmingизд. Д. Тернер, Аддисон-Уэсли, стр. 17–42,Woohoo. В это время Kent.AC.UK/people/Cetaphil…

  6. TensorFlow™: платформа машинного обучения с открытым исходным кодом для всех, www.tensorflow.org/, 12 мая 2018 г.

Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,товар,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.