Функциональное программирование: рефакторинг, замыкания и функции высшего порядка
Продолжнаем изучать функциональное программирование и его основные концепции, а также учимся работать с кодом в функциональном ключе.
Рефакторинг
Взгляните на пример кода на JavaScript:
function validateSsn(ssn) { if (/^\d{3}-\d{2}-\d{4}$/.exec(ssn)) console.log('Valid SSN'); else console.log('Invalid SSN'); } function validatePhone(phone) { if (/^\(\d{3}\)\d{3}-\d{4}$/.exec(phone)) console.log('Valid Phone Number'); else console.log('Invalid Phone Number'); }
Две эти функции практически идентичны. Вместо копирования и изменения validateSsn, давайте попробуем объединить функции в одну, а различия вынести в переменные. Введем значения value, regular expression и message:
function validateValue(value, regex, type) { if (regex.exec(value)) console.log('Invalid ' + type); else console.log('Valid ' + type); }
Параметры ssn и phone представлены в виде value. Регулярные выражения /^\d{3}-\d{2}-\d{4}$/ и /^\(\d{3}\)\d{3}-\d{4}$/ вынесены в regex. Части сообщения, отвечающие за вид проверки вынесены в type.
Иметь одну функцию лучше, чем две. И тем более, лучше, чем 3 и больше. Только представьте, какого будет искать ошибку, которая могла случайно закрасться в одну из десятков похожих функций во время ручных правок.
Но случается, что вы видите имеете такой код:
function validateAddress(address) { if (parseAddress(address)) console.log('Valid Address'); else console.log('Invalid Address'); } function validateName(name) { if (parseFullName(name)) console.log('Valid Name'); else console.log('Invalid Name'); }
Здесь parseAddress и parseFullName – функции, которые принимают строку и возвращают true, если она разбирается. По аналогии с прошлым примером, мы можем ввести value для address и name, и type для ‘Address’ или ‘Name’. Но как передать в параметре функцию?
Функции высшего порядка
Многие языки программирования не поддерживают передачу функций в качестве параметра. В функциональном программировании, функция может передаваться в другую функцию наравне с переменными.
Хотя JavaScript и не является чистым функциональным языком, в нем можно проделывать некоторые функциональные трюки, в том числе, передавать функцию в качестве параметра.
Для нашего примера введем параметр parseFunc, в который будем передавать функции для обработки строк.
function validateValueWithFunc(value, parseFunc, type) { if (parseFunc(value)) console.log('Invalid ' + type); else console.log('Valid ' + type); }
То, что мы получили, называется функцией высшего порядка.
Теперь, мы можем вызывать новую функцию высшего порядка для каждого из четырех примеров функций выше, потому что в JavaScript Regex.exec будет также возвращать true, если строка пройдет проверку:
validateValueWithFunc('123-45-6789', /^\d{3}-\d{2}-\d{4}$/.exec, 'SSN'); validateValueWithFunc('(123)456-7890', /^\(\d{3}\)\d{3}-\d{4}$/.exec, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, ‘Name');
Теперь код выглядит лучше, но регулярные выражения все еще занимают много места. Давайте исправим и это:
var parseSsn = /^\d{3}-\d{2}-\d{4}$/.exec; var parsePhone = /^\(\d{3}\)\d{3}-\d{4}$/.exec; validateValueWithFunc('123-45-6789', parseSsn, 'SSN'); validateValueWithFunc('(123)456-7890', parsePhone, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, ‘Name');
Код стал опрятней, но регулярных выражений может потребоваться значительно больше, чем два. И для каждого такого выражения нужно создавать отдельную переменную и не забыть добавить в конце .exec, а забыть об этом очень просто. Давайте снова улучшим код, чтобы еще немного облегчить себе жизнь:
function makeRegexParser(regex) { return regex.exec; } var parseSsn = makeRegexParser(/^\d{3}-\d{2}-\d{4}$/); var parsePhone = makeRegexParser(/^\(\d{3}\)\d{3}-\d{4}$/); validateValueWithFunc('123-45-6789', parseSsn, 'SSN'); validateValueWithFunc('(123)456-7890', parsePhone, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, 'Name');
Так, мы создали функцию, которая принимает регулярное выражение и возвращает функцию для парсинга с .exec на конце.
Функция, как результат работы функции
Это незначительное улучшение, но хороший пример функции высшего порядка, которая возвращает другую функцию.
Вот еще один пример такой функции:
function makeAdder(constantValue) { return function adder(value) { return constantValue + value; }; }
Здесь функция makeAdder принимает constantValue и возвращает adder, функцию, которая прибавляет константу к любому значению, которое получает. Вот как можно это использовать:
var add10 = makeAdder(10); console.log(add10(20)); // выведет 30 console.log(add10(30)); // выведет 40 console.log(add10(40)); // выведет 50
Мы создали функцию add10, передав константу 10 функции makeAdder, которая вернет значение + 10 от любого исходного.
Обратите внимание, что adder имеет доступ к constantValue даже после того, как makeAddr вернула значение. Так происходит потому, что constantValue была в своей области, когда adder была создана.
Такое поведение важно, потому что без него функции, возвращающие функции были бы не так полезны. Важно понимать как это работает и как называется такое поведение.
Это называется «замыкания».
Замыкания
Ниже показан выдуманный пример функции, которая использует замыкания.
function grandParent(g1, g2) { var g3 = 3; return function parent(p1, p2) { var p3 = 33; return function child(c1, c2) { var c3 = 333; return g1 + g2 + g3 + p1 + p2 + p3 + c1 + c2 + c3; }; }; }
В этом примере child имеет доступ к своим переменным, переменным родителя и переменным родителя родителя.
parent имеет доступ к своим переменным и переменным своего родителя.
grandParent имеет доступ только к своим переменным.
Так это можно использовать:
var parentFunc = grandParent(1, 2); // вернет parent() var childFunc = parentFunc(11, 22); // вернет child() console.log(childFunc(111, 222)); // выведет 738 // 1 + 2 + 3 + 11 + 22 + 33 + 111 + 222 + 333 == 738
Здесь parentFunc удерживает область parent с того момента, как grandParent возвращает parent.
Также, childFunc удерживает область child с того момента, как parentFunc, которая просто parent, возвращает child.
Когда функция создана, все переменные в ее области во время создания доступны на время жизни функции. Функция существует столько, сколько существуют ссылки на нее. Например, область функции child существует, пока на нее ссылается childFunc.
Замыкания – полезный инструмент, но в JavaScript он может приносить проблемы, когда переменные меняют значения. К счастью, в функциональном программировании переменные значения не меняют.