Skip to content

Latest commit

 

History

History
312 lines (206 loc) · 24.6 KB

ch3-ru.md

File metadata and controls

312 lines (206 loc) · 24.6 KB

Глава 3: Чистое счастье с Чистыми функциями

Как же хорошо снова быть чистым

Нам совершенно необходимо чётко понять концепцию чистой функции.

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

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

var xs = [1,2,3,4,5];

// чистая
xs.slice(0,3);
//=> [1,2,3]

xs.slice(0,3);
//=> [1,2,3]

xs.slice(0,3);
//=> [1,2,3]


// не чистая
xs.splice(0,3);
//=> [1,2,3]

xs.splice(0,3);
//=> [4,5]

xs.splice(0,3);
//=> []

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

Приведу ещё один пример.

// не чистая
var minimum = 21;

var checkAge = function(age) {
  return age >= minimum;
};

// чистая
var checkAge = function(age) {
  var minimum = 21;
  return age >= minimum;
};

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

В данном примере это может показаться мелочью, но зависимость системы от своего состояния — это один из самых важных компонентов её сложности[^http://www.curtclifton.net/storage/papers/MoseleyMarks06a.pdf]. Функция checkAge может возвращать разные значения, в зависимости от внешнего фактора, что не только исключает её из класса чистых, но ещё и затрудняет её понимание, каждый раз когда мы пытаемся проанализировать код.

Её чистый вариант, наоборот, полностью самодостаточен. Мы также можем сделать переменную minimum константой, что позволит сохранить чистоту, так как состояние никогда не изменится. Чтобы добиться этого нам потребуется использовать метод freeze объекта Object.

var immutableState = Object.freeze({
  minimum: 21
});

Побочные эффекты могут включать...

Давайте более подробно разберёмся с этими «побочными эффектами», чтобы развить нашу интуицию. Что же именно скрывается за этими несомненно гнусными побочными эффектами, упомянутыми в определении чистой функции? Мы будем считать эффектом всё, что вызывает какие-либо вычисления, кроме результата самой функции.

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

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

Побочные эффекты могут включать (но не ограничиваться):

  • изменения в файловой системе
  • вставку записи в базу данных
  • выполнение http-запроса
  • мутации
  • вывод на экран / запись в лог
  • получение данных от пользователя
  • выполнение запроса к DOM
  • получение доступа к состоянию системы

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

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

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

Давайте разберёмся, почему я так настаиваю на одном и том же результате для одинаковых аргументов. Садитесь за парту ровно, я напомню вам школьную программу по математике 8-го класса.

Математика 8-го класса

С сайта mathisfun.com:

Функция — это взаимоотношение между величинами: каждое из входных значений возвращает одно и только одно выходное.

Другими словами, функция — это всего лишь отношение между двумя величинами: аргументом и значением. Хотя и каждому аргументу ставится в соответствие единственное значение функции, обратное не верно. Так, функция, вызванная с разными аргументами может возвращать одно и то же значение. На диаграмме ниже изображена вполне законная функция xy.

[^http://www.mathsisfun.com/sets/function.html]

Для сравнения, следующая диаграмма показывает отношение, которое не является функцией. Так как для аргумента 5 есть несколько значений:

[^http://www.mathsisfun.com/sets/function.html]

Функцию можно описать как набор упорядоченных пар (аргумент, значение): [(1,2), (3,6), (5,10)][^Похоже, что эта функция удваивает аргумент].

Или в виде таблицы:

Аргумент Значение
1 2
2 4
3 6

Или в виде графика, где по оси x отложен аргумент, а по оси y — значение:

Нам совершенно не важны детали реализации механизма функции, нам достаточно знать, что аргумент диктует значение. Так как функции являются всего лишь отображением аргумента на значение, то мы можем просто-напросто записывать литералы объекта с [], вместо ().

var toLowerCase = {"A":"a", "B": "b", "C": "c", "D": "d", "E": "e", "D": "d"};

toLowerCase["C"];
//=> "c"

var isPrime = {1:false, 2: true, 3: true, 4: false, 5: true, 6:false};

isPrime[3];
//=> true

Согласен, возможно вы захотите вычислять значение функции, а не просто вписывать вручную все возможные пары аргументов-значений.[^Кто-то из вас может заметить: «а как же функции многих переменных?» Действительно, с точки зрения математической строгости может показаться несколько странным, что мы не обговорили этот момент. На данный момент, будем считать несколько аргументов за один, являющийся массивом. Когда мы изучим *каррирование*, мы сможем легко пользоваться строгим математическим определением функции.]

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

Место для чистоты

Кешируемость

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

var squareNumber  = memoize(function(x){ return x*x; });

squareNumber(4);
//=> 16

squareNumber(4); // возвращает результат из кеша для аргумента 4
//=> 16

squareNumber(5);
//=> 25

squareNumber(5); // возвращает результат из кеша для аргумента 5
//=> 25

Ниже я написал упрощённую реализацию мемоизации, в интернете вы можете найти более надёжную версию.

var memoize = function(f) {
  var cache = {};

  return function() {
    var arg_str = JSON.stringify(arguments);
    cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
    return cache[arg_str];
  };
};

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

var pureHttpCall = memoize(function(url, params){
  return function() { return $.getJSON(url, params); }
});

Интересный момент, мы не делаем http-запрос, вместо него мы возвращаем функцию, которая сделает, когда будет вызвана. Эта функция является чистой, так как всегда для одинакового аргумента возвращает одно и то же значение — функцию, которая сделает http-запрос по заданным url и params.

Функция memoize работает нормально, хотя она и не закеширует результат http-запроса, а закеширует сгенерированную функцию.

Пока что memoize не кажется очень полезной, однако скоро мы изучим некоторые хитрости, которые позволят кешировать любую функцию, вне зависимости от её чистоты.

Переносимость / Самодокументированность

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

// не чистая
var signUp = function(attrs) {
  var user = saveUser(attrs);
  welcomeUser(user);
};

// чистая
var signUp = function(Db, Email, attrs) {
  return function() {
    var user = saveUser(Db, attrs);
    welcomeUser(Email, user);
  };
};

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

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

Хочу также отметить, что мы вынуждены «внедрять» зависимости (передавать их в качестве аргументов), что делает наше приложение куда более гибким. Это происходит за счёт параметризации вашего приложения[^Не волнуйтесь, мы научимся делать это менее скучно, чем это звучит]. Если вы хотите поменять базу данных, то вам всего-навсего потребуется вызвать функцию, передав новую базу в качестве аргумента. Когда вы будете писать новое приложение и захотите переиспользовать надёжную чистую функцию, вы можете просто передать ей те Db и Email, которые актуальны для вас.

В среде JavaScript портативность может означать сериализацию и отправку функции через сокет или же запуск всего приложения с помощью web worker'ов. Портативность — мощная штука.

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

Вспомните последний раз, когда вы копировали метод из одного приложения в другое. Давно это было? Одна из моих самых любимых цитат была произнесена создателем языка Erlang, Джо Армстронгом: «Проблема объектно-ориентированных языков в неявной среде, которая их окружает. Вы хотели получить банан, а получили гориллу с бананом... И все джунгли».

Тестирумость

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

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

Разумность

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

Поскольку чистые функции всегда возвращают один и тот же результат для одинаковых аргументов — они сохраняют прозрачность ссылок. Давайте обратимся к примеру.

var decrementHP = function(player) {
  return player.set("hp", player.hp-1);
};

var isSameTeam = function(player1, player2) {
  return player1.team === player2.team;
};

var punch = function(player, target) {
  if(isSameTeam(player, target)) {
    return target;
  } else {
    return decrementHP(target);
  }
};

var jobe = Immutable.Map({name:"Jobe", hp:20, team: "red"});
var michael = Immutable.Map({name:"Michael", hp:20, team: "green"});

punch(jobe, michael);
//=> Immutable.Map({name:"Michael", hp:19, team: "green"})

Функции decrementHP, isSameTeam и punch являются чистыми и, следовательно, ссылочно-прозрачными. Мы можем воспользоваться техникой рассуждения о равном, в которой мы заменяем равное равным, чтобы проанализировать код. Это примерно то же самое, что и в уме просчитывать код, не учитывая тонкостей его программного выполнения. Давайте поиграемся с кодом, используя прозрачность ссылок.

Начнём с того, что встроим функцию isSameTeam в punch.

var punch = function(player, target) {
  if(player.team === target.team) {
    return target;
  } else {
    return decrementHP(target);
  }
};

Так как наши данные не меняются, то мы просто заменим переменные player.team и target.team на их значения.

var punch = function(player, target) {
  if("red" === "green") {
    return target;
  } else {
    return decrementHP(target);
  }
};

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

var punch = function(player, target) {
  return decrementHP(target);
};

И, если мы встроим decrementHP, мы поймём, что вызов функции punch становится вызовом функции по уменьшению hp на 1 единицу.

var punch = function(player, target) {
  return target.set("hp", target.hp-1);
};

Способность анализировать код таким образом чрезвычайно удобна для рефакторинга и понимания того, что код на самом деле делает. Мы уже использовали эту технику раньше, когда рефакторили программу про стаи чаек (свойства сложения и умножения).

Параллелизм

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

Это вполне применимо как к потоковому JS на сервере, так и к браузерному, с использованием web workers, хотя их особенно и не используют из-за сложностей, возникающих при работе с нечистыми функциями.

Итог

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

Писать программы с чистыми функциями без знания и использования некоторых инструментов нелегко. Нам придётся жонглировать данными, постоянно передавая аргументы, нам запрещено использовать состояние, не говоря уже об эффектах. Как вы смотрите на такой мазохистский подход? Давайте изучим новый инструмент — каррирование.

Глава 4: Каррирование