Простые функциональные методы программирования на TypeScript

  • 26 ноября, 16:44
  • 2425
  • 0

Согласно Википедии:

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

Простые функциональные методы программирования на TypeScriptСледовательно, в функциональном программировании есть два очень важных правила:

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

Кроме этого, ниже представлены концепции функционального программирования, мы коснемся их ниже.

  1. Функции первого класса и высшего порядка
  2. Замыкание
  3. Карринг
  4. Рекурсия
  5. Ленивые оценки
  6. Ссылочная прозрачность

Функциональное программирование в TypeScript

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

Функции первого класса и высшего порядка

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

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

В TypeScript это довольно легко сделать

type mapFn = (it: string) => number;

// The higher-order-function takes an array and a function as arguments
function mapForEach(arr: string[], fn: mapFn): number[] {
    const newArray: number[] = [];
    arr.forEach(it => {
        // We are executing the method passed
        newArray.push(fn(it));
    });
    return newArray;
}

const list = ["Orange", "Apple", "Banana", "Grape"];

// we are passing the array and a function as arguments to mapForEach method.
const out = mapForEach(list, (it: string): number => it.length);

console.log(out); // [6, 5, 6, 5]

Но тогда в JavaScript / TypeScript мы могли бы просто сделать это, используя встроенные функциональные методы, такие как map, redu и так далее.

const list = ["Orange", "Apple", "Banana", "Grape"];

// we are passing a function as arguments to the built-in map method.
const out = list.map(it => it.length);

console.log(out); // [6, 5, 6, 5]

Замикание и каррирование также возможны в TypeScript

// this is a higher-order-function that returns a function
function add(x: number): (y: number) => number {
    // A function is returned here as closure
    // variable x is obtained from the outer scope of this method and memorized in the closure
    return (y: number): number => x + y;
}

// we are currying the add method to create more variations
var add10 = add(10);
var add20 = add(20);
var add30 = add(30);

console.log(add10(5)); // 15
console.log(add20(5)); // 25
console.log(add30(5)); // 35

Есть также множество встроенного декларативных функций высших порядков, как map, reduce, forEach, filter и так далее. Есть также много библиотек, которые предоставляют функциональные интерфейсы для использования в TypeScript / JavaScript.

Чистые функции

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

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

function sum(a: number, b: number): number {
    return a + b;
}

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

const holder = {};

function sum(a: number, b: number): number {
    let c = a + b;
    holder[`${a}+${b}`] = c;
    return c;
}

Поэтому постарайтесь, чтобы ваши функции были чистыми и простыми. Используя такие инструменты, как ESLint и typcript -eslint, можно применить их.

Рекурсия

Функциональное программирование способствует рекурсии по циклу. Давайте посмотрим пример для вычисления факториала числа.

В традиционном итеративном подходе:

function factorial(num: number): number {
    let result = 1;
    for (; num > 0; num--) {
        result *= num;
    }
    return result;
}

console.log(factorial(20)); // 2432902008176640000

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

const factorial = (num: number): number =>
    num == 0 ? 1 : num * factorial3(num - 1);

console.log(factorial(20)); // 2432902008176640000

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

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

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

const factorialTailRec = (num: number): number => factorial(1, num);

const factorial = (accumulator: number, val: number): number =>
    val == 1 ? accumulator : factorial(accumulator * val, val - 1);

console.log(factorialTailRec(20)); // 2432902008176640000

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

Ленивая оценка

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

Возьмите этот пример:

function add(x: number): number {
    console.log("executing add"); // this is printed since the functions are evaluated first
    return x + x;
}

function multiply(x: number): number {
    console.log("executing multiply"); // this is printed since the functions are evaluated first
    return x * x;
}

function addOrMultiply(
    add: boolean,
    onAdd: number,
    onMultiply: number
): number {
    return add ? onAdd : onMultiply;
}

console.log(addOrMultiply(true, add(4), multiply(4))); // 8
console.log(addOrMultiply(false, add(4), multiply(4))); // 16

Это даст следующий вывод, и мы видим, что обе функции выполняются всегда

executing add
executing multiply
8
executing add
executing multiply
16

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

function add(x: number): number {
    console.log("executing add");
    return x + x;
}

function multiply(x: number): number {
    console.log("executing multiply");
    return x * x;
}

type fnType = (t: number) => number;
// This is now a higher-order-function hence evaluation of the functions are delayed in if-else
function addOrMultiply(
    add: boolean,
    onAdd: fnType,
    onMultiply: fnType,
    t: number
): number {
    return add ? onAdd(t) : onMultiply(t);
}
console.log(addOrMultiply(true, add, multiply, 4));
console.log(addOrMultiply(false, add, multiply, 4));

Мы можем видеть, что были выполнены только необходимые функции

executing add
8
executing multiply
16

Или 

const cachedAdded = {};
function add(x: number): number {
    if (cachedAdded[x]) {
        return cachedAdded[x];
    }
    console.log("executing add");
    const out = x + x;
    cachedAdded[x] = out;
    return out;
}
const cachedMultiplied = {};
function multiply(x: number): number {
    if (cachedMultiplied[x]) {
        return cachedMultiplied[x];
    }
    console.log("executing multiply");
    const out = x * x;
    cachedMultiplied[x] = out;
    return out;
}

function addOrMultiply(
    add: boolean,
    onAdd: number,
    onMultiply: number
): number {
    return add ? onAdd : onMultiply;
}

console.log(addOrMultiply(true, add(4), multiply(4))); // 8
console.log(addOrMultiply(false, add(4), multiply(4))); // 16

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

executing add
executing multiply
8
16

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

Ссылочная прозрачность

Из Википедии:

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

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

Пример ниже будет выдавать ошибку

const list = ["Apple", "Orange", "Banana", "Grape"]
list = ["Earth", "Saturn"];

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

const list = ["Apple", "Orange", "Banana", "Grape"];

list.push("Earth"); // will mutate the list
list.push("Saturn"); // will mutate the list

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

Однако с TypeScript мы можем использовать специальные сопоставленные типы, чтобы сделать объекты доступными только для чтения и, следовательно, избежать случайных мутаций данных, которые обнаруживаются во время компиляции. 

const list: Readonly<string[]> = ["Apple", "Orange", "Banana", "Grape"];
list.push("Earth"); // will cause compilation error
или
const list: ReadonlyArray<string> = ["Apple", "Orange", "Banana", "Grape"];

list.push("Earth"); // will cause compilation error

Другие методы, которым нужно следовать, - это использование Object.freeze или встроенных методов, таких как map, Reduce, Filter и т.д., Поскольку они не изменяют данные. Мы также можем использовать плагин ESlint для ограничения мутаций.

Вывод

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


0 комментариев
Сортировка:
Добавить комментарий