Higher order functions jest kolejnym z elementów programowania funkcyjnego. Poprzednia publikacja dotyczyła założeń pisania czystych funkcji zgodnie z koncepcją programowania funkcyjnego.

Zatem zaczynamy…

Czym są higher order functions?

Higher order function jest funkcją, która przyjmuje jako parametr inną funkcję lub funkcję zwraca.

Pierwszy przykład, czyli funkcja przyjmuje funkcję jako parametr

Doskonałym przykładem higher order function będzie .map() (https://developer.mozilla.org/pl/docs/Web/JavaScript/Referencje/Obiekty/Array/map) lub .filter()(https://developer.mozilla.org/pl/docs/Web/JavaScript/Referencje/Obiekty/Array/filter).

let numbers = [1, 2, 3];
let multiple = numbers.map(function(number) {
    return number * 2;
});

console.log(multiple); // [2, 4, 6]

Jak widać, funkcja .map(), jako argument przyjmuje inną funkcję.

Funkcja zwraca inną funkcję

Wyobraźmy sobie, że chcemy zbadać podaną jako argument liczbę X i sprawdzić, czy jest większa niż Y, zwracając true lub false. Jest to trochę abstrakcyjny przypadek, bo wystarczy zwykłe porównanie, ale być może taki prosty przykład pozwoli lepiej zrozumieć, w jaki sposób można ugryźć temat wykorzystując higher order function.

W standardowym podejściu można oczywiście stworzyć funkcję, która będzie miała zadeklarowaną wartość „na sztywno”.

let greaterThan10 = function(number) {
  if (number > 10) {
    return true;
  }

  return false;
}

greaterThan10(9); // false

Można stworzyć funkcję, która będzie przyjmowała dwa argumenty (pierwszy to wartość do porównania, druga to wartość graniczna).

let greaterThan = function(number, compare) {
  if (number > compare) {
    return true;
  }

  return false;
}

greaterThan(9, 10); // false

Można też stworzyć higher order function… (w przykładzie celowo użyłem takiej składni, aby zwracana funkcja była bardziej widoczna 🙂 )

let greaterThan = function(compare) {
  return function(number) { 
    return number > compare;
  }
}

let greaterThan10 = greaterThan(10);
let greaterThan8 = greaterThan(8);

greaterThan10(9); // false
greaterThan8(9); // true

Tutaj wyłania nam się jedna z zalet higher order function, czyli braku powtarzalności kodu.

Kompozycja

Kolejnym zastosowaniem jest wydzielanie fragmentów logiki do funkcji, które później możemy połączyć ze sobą w większe fragmenty kodu na zasadzie kompozycji.

Przykład

Wyobraźmy sobie, że obsługujemy piekarnię i na podstawie listy produktów przeprowadzamy różne operacje, np. analizujemy dane o produktach…

let products = [
  { type: 'bread', price: 10 },
  { type: 'bread', price: 15 },
  { type: 'bread', price: 20 },
  { type: 'cake', price: 100 },
  { type: 'cake', price: 120 },
  { type: 'cake', price: 140 }
];

Chcemy wybrać wszystkie produkty chlebowe, ciasta, produkty powyżej podanej ceny, wybrać średnią cenę itd…

Na początek, wybieramy typ produktu

let isBread = product => product.type === 'bread';
let isCake = product => product.type === 'cake';

Funkcje te przydadzą nam się do kompozycji logiki i wykorzystania higher order function.

let getBread = products => (
  products.filter(isBread)
);

let getCake = products => (
  products.filter(isCake)
);

console.log(getBread(products)); 
// [ { type: 'bread', price: 10 }, 
// { type: 'bread', price: 15 }, 
// { type: 'bread', price: 20 } ]
console.log(getCake(products)); 
// [ { type: 'cake', price: 100 },
// { type: 'cake', price: 120 },
// { type: 'cake', price: 140 } ]

oraz produkty, o cenie większej niż price

let priceHigher = (products, price) => {
    return products.filter(
        (product) => (product.price > price)
    )
};

Wyobraźmy sobie, że chcemy teraz wybrać średnią cenę danego typu produktu.

let avgPrice = (products) => {
    return products.reduce((total, currentProduct) => (
        total + currentProduct.price
    ), 0) / products.length
}

console.log(avgPrice(getBread(products))); // 15 ((10 + 15 + 20) / 3)
console.log(avgPrice(getCake(products))); // 120 ((100 + 120 + 140) / 3)

oraz na sam koniec…

  • średnią cenę produktów
  • typu chleb,
  • których cena jest większa niż 13…
avgPrice(priceHigher(getBread(products), 13));

Jeżeli chcecie poczytać więcej na temat takiego łączenia funkcji, można poszukać czegoś pod frazą function composition.

Dzięki kompozycji (function composition) oraz higher order functions udało nam się wykorzystać napisane wcześniej funkcje w naszej logice. Funkcje te mogą być dalej używane przez inną logikę. Nie powielamy kodu, kod jest czytelniejszy, a funkcje wykonują jedną część logiki, przez co są mniejsze, łatwiej się je czyta i analizuje.

Na koniec ostatni przykład „na szybko” bez użycia HOC.
Na pewno można napisać to jeszcze na kilka sposobów i zapewne ładniej, ale poniższy przykład już pokazuje, że wykorzystanie HOC daje nam przewagę, a przede wszystkim umożliwia wykorzystanie kodu w bardziej elastyczny sposób niż ten poniżej…

let avgPriceProductsHigher = products => {
    let breadPriceHigher = products.filter(
        (product) => product.type === 'bread' && product.price > 13
    )

    return breadPriceHigher.reduce((total, currentProduct) => (
        total + currentProduct.price
    ), 0) / breadPriceHigher.length
}

console.log(avgPriceProductsHigher (products));

Źródła:
Obraz: https://unsplash.com/photos/1LLh8k2_YFk