
Хорошо, значит, это могло произойти раз или два раньше. Вы в десятый раз пытались прочитать книгу о Haskell.
Потом заговорили о составлении функций. Или функциональный состав.
Или они пошли на охоту за доброй волей на твою задницу с чем-то вроде: f(g(h(i(x))))
Давайте проигнорируем все это и начнем заново.
Привет! Функциональное программирование - это простая основа того, как должно работать.
Сегодня мы поговорим об одном аспекте функционального программирования: чистых функциях.
Мы собираемся поговорить о том, как чистые функции означают проверяемые функции.
В классе алгебры у вас была такая математическая функция: 2x + y = 50, x = 4
С этим довольно легко решить, не так ли?
2x + y = 50, x = 4
2(4) + y = 50
8 + y = 50
y = 42
Вы знаете, это неплохо! Если x равно 4, тогда y всегда 42. Он никогда не меняется. В качестве ответа мы всегда получаем 42.
Теперь давайте изменим эту математику, чтобы она больше походила на программу:
2x + y = 50, x = someObject.getX()
2(4) + y = 50
8 + y = 50
y = 42
console.log(x.y)
database.save(y)
О Господи, у нас сейчас проблемы! Если мы запустим это, всегда ли мы получим 42? Если вы сказали «да», ответ отрицательный!
Что может пойти не так? Ах, позволь мне посчитать пути ...
- Что делать, если вызов
someObject.getX()вызывает ошибку? - Что, если
console.logвыдает ошибку? - У меня плохие новости,
console.log(x.y)выдает ошибку, потому что x должен быть числом, а не объектом со свойствомy - Что, если
database.save(y)выдаст ошибку до того, как мы сможем вернуть y?
Так в чем проблема?
Проблема в том, что наша функция не была чистой. Чистая функция работает только с переданными ей аргументами.

Итак, как бы здесь выглядела чистая функция? Как насчет этого:
const y = x => 50 - (2 * x)
Хорошо, теперь понятно, я упростил 2x + y = 50 до y = 50 — 2x, но я не об этом говорю. В этом примере нас не интересует алгебра.
Но вот в чем дело. В этой функции: x => 50 — (2 * x)
Мы ссылаемся только на аргументы в левой части функции. Нет global.causeTrouble, нет подключения к базе данных или регистратора.
И действительно, y(4) === 42 каждый забытый Богом раз, когда вы запускаете его.
Что делает его отличным (и легким) для тестирования.
expect(y(4)).to.equal(42) // every time expect(y(4)).to.equal(42) // every time expect(y(4)).to.equal(42) // every time expect(y(4)).to.equal(42) // every time, same as maths!
Теперь у этого примера есть одна проблема, и это реальный мир, место, в котором мы, программисты, на самом деле живем.
Приятно создать чистую функцию, которая выполняет только арифметические операции. Но как насчет ведения журнала, обработки ошибок и баз данных?
Так это непрактично, правда? Позвольте мне сказать вам кое-что, все будет хорошо.
Допустим, нам действительно нужна вся эта чушь (потому что бизнес-пользователи):
2x + y = 50, x = someObject.getX()
2(4) + y = 50
8 + y = 50
y = 42
console.log(y)
database.save(y)
Как, черт возьми, мы собираемся это сделать сейчас? На самом деле это довольно просто.
Мы по-прежнему будем использовать чистую функцию, которую мы написали минуту назад. Но мы вызовем его в другой функции, которая делает грязные вещи:
try {
const x = someObject.getX() // assume it gets 4
const answer = y(x) // still pure so 42
console.log(answer)
database.save(answer)
return answer
} catch (e) {
console.log(e)
}
На самом деле наша y функция по-прежнему чиста, но все, что мы сделали, это переместили нечистый код вверх по стеку. Итак, что мы решили?
Ну, во-первых, y все еще работает! И модульные тесты будут доказывать это навсегда, так что это победа.
У нас есть новая функция, которую не очень хорошо тестировать, так как же нам ее решить?
Мы решаем это, создав еще одну чистую функцию! Помните, нам разрешено использовать только то, что передано. Ничего другого не существует во вселенной вне нашей функции. Только аргументы.
const solveY = (someObject, y, log, database) => {
try {
const x = someObject.getX()
const answer = y(x)
log(answer)
database.save(answer)
return answer
} catch (e) {
log(e)
}
}
Хорошо, это хороший первый шаг. Теперь наша функция использует только то, что исходит из аргументов. Это довольно чисто! Не правда ли?

Мы не можем гарантировать, что solveY будет возвращать одно и то же каждый раз. Ошибки, тайм-ауты и все остальное все равно будут.
Это побочные эффекты. Функция с побочными эффектами не чистая. Однако в рамках этого руководства не будем обращать внимания на побочные эффекты. Это еще одна проблема на другой день.
У нас действительно есть чистая функция в том смысле, что она не работает с переменными вне своей области видимости. Это означает, что наша функция поддается тестированию.
Сегодняшняя наша цель - возможность тестирования через чистоту.
Мы не можем гарантировать, что solveY всегда будет возвращать 42. Но здесь нет необходимости гарантировать это. Наша y функция уже делает это.
Что мы можем сделать, так это гарантировать, что solveY сделает свою работу. Например, мы можем шпионить за getX и убедиться, что он вызван, и даже выдать фальшивый ответ:
it('should call someObject.getX to get the x value', () => {
const someObject = { getX: sinon.stub().returns(4) }
const y = sinon.stub()
const log = sinon.stub()
const database = { save: sinon.stub() }
solveY(someObject, y, log, database)
expect(someObject.getX.called()).to.equal(true)
})
Нам также необходимо написать тест, который гарантирует, что при возникновении ошибки в solveY мы регистрируем ошибку:
it('should log when an error happens', () => {
const mockError = new Error('uh oh')
const someObject = { getX: sinon.stub().throws(mockError) }
const y = sinon.stub()
const log = sinon.stub()
const database = { save: sinon.stub() }
solveY(someObject, y, log, database)
expect(log.calledWith(mockError)).to.equal(true)
})
И так далее! Мы никогда не сможем гарантировать, что вызовы базы данных будут работать без ошибок. Тем не менее, мы можем протестировать нашу обработку ошибок.
Одна критика, которую я испытываю к нашему текущему примеру, заключается в том, что мы передаем solveY слишком много информации.
Мы передаем someObject, чтобы можно было вызвать getX.. Но мы не используем someObject ни для чего другого. Так почему бы вместо этого не передать getX? Действительно, почему.
То же самое и с объектом database. Все, что мы используем, - это вызвать .save, так почему бы вместо этого не передать save?
Это идея функционального программирования! Избавьтесь от классов и методов классов и вместо этого передавайте чистые функции.
В моем Javascript, если я никогда не использую class или this ключевое слово, то это был хороший день!

Итак, давайте посмотрим, как выглядят передающие функции:
const solveY = (getX, y, log, saveInDatabase) => {
try {
const x = getX()
const answer = y(x)
log(answer)
saveInDatabase(answer)
return answer
} catch (e) {
log(e)
}
}
// And refactor the tests
it('should call getX to get the x value', () => {
const getX = sinon.stub().returns(4)
const y = sinon.stub()
const log = sinon.stub()
const saveInDatabase = sinon.stub()
solveY(getX, y, log, saveInDatabase)
expect(getX.called()).to.equal(true)
})
Это не сильно отличается от прежнего. Фактически, мы уже передавали функции для y и log
Я делаю это упражнение по удалению someObject и database неспроста. Программисты, кажется, упираются в кирпичную стену, когда дело доходит до передачи функций в качестве аргументов.
Но передача объекта и последующий вызов метода для этого объекта настолько распространены, что, кажется, никто не задумывается дважды.
Я показываю вам, что передача функций ничем не отличается и, по сути, проще. Моки проще. Вызвать функцию проще, будь то тест или реальный код. И твоя жизнь лучше.
Я показал, как чистые функции делают ваш код тестируемым. Если ваш код поддается тестированию, вам следует его протестировать! Если вы тестируете свой код, он делает то, что вы ожидаете. Если он делает то, что вы ожидаете, вы просыпаетесь на 2 часа ночи меньше.
Я знаю, что просыпаться сейчас в моде, но я люблю спать.
И, наконец, мы сделали аргумент, что нет необходимости передавать объект, когда функция будет работать. Я полагаю, что классы действительно обеспечивают некоторую организацию, но для этого у нас есть модули.
Большим злом объектно-ориентированного программирования является ключевое слово this. В Javascript функции могут быть связаны не с тем. И тогда this не то, что вы ожидали.
Такое поведение может вызывать некоторые странные ошибки. Даже старшие разработчики могут запутаться, если не знают волшебного танца.
И прежде чем вы подумаете: «Это вызывает проблемы только в Javascript, а не в моей любимой Java», подумайте еще раз.
Попав в объектно-ориентированную экономику, выхода нет! У ваших функций есть this везде. Ваша функция нечиста еще до того, как вы начнете вводить в нее this.
Поскольку this не является аргументом функции, вы не можете издеваться над ним, если не будете издеваться над всем объектом. Вы не можете смоделировать весь объект, если не смоделируете все зависимости, необходимые для создания вашего фиктивного объекта.
Допустим, для этого конкретного класса требуется объект RickyBobbyConfigurator в конструкторе. Неважно, что ваша функция проста. Неважно, что все, что вы хотите передать, - это число.
Вам нуженRickyBobbyConfigurator и все!
Есть волшебные вещи! Существуют фреймворки, которые могут автоматически смоделировать вещи и сделать это менее болезненным. Это хорошо до тех пор, пока вам не нужно немного изменить магию, и в ней не будет поддержки для нужного вам заклинания.
Вы тестируете функцию, и все, что вам нужно, это банан. Я не могу сказать это правильно, поэтому вот знаменитая цитата Джо Армстронга (Erlang):
Я думаю, что отсутствие возможности повторного использования присуще объектно-ориентированным языкам, а не функциональным языкам. Потому что проблема объектно-ориентированных языков в том, что у них есть вся эта неявная среда, которую они носят с собой. Вы хотели банан, но получили гориллу, держащую банан и целые джунгли.
Если у вас есть ссылочно прозрачный код, если у вас есть чистые функции - все данные поступают в его входные аргументы, все выходит и не оставляет никакого состояния - это невероятно многоразовое использование.
Я не могу объяснить это лучше, чем это.
Хорошо! Теперь, когда вы ненавидите ООП и любите функциональность, вам остается только одерживать победу и получать прибыль!

Если вам понравилась эта статья, пожалуйста, подпишитесь на меня прямо здесь, в среде или…
