Функциональное программирование: рефакторинг, замыкания и функции высшего порядка

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

Рефакторинг

Взгляните на пример кода на 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 он может приносить проблемы, когда переменные меняют значения. К счастью, в функциональном программировании переменные значения не меняют.

Другие статьи по теме

70 ресурсов по функциональному программированию

Принципы функционального программирования: почему это важно

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
30 июня 2018

Шаблоны проектирования в Python: для стильного кода

Многие шаблоны проектирования встроены в Python из коробки, а другие очень ...