Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Programowanie funkcyjne w JavaScript/TypeScript

Podobnie jak starożytni matematycy dążyli do eleganckich, czystych rozwiązań matematycznych, programowanie funkcyjne koncentruje się na czystych funkcjach, niemutowalności i kompozycji. W tej lekcji poznamy fundamentalne koncepcje programowania funkcyjnego i jak stosować je w JavaScript/TypeScript, aby tworzyć bardziej przewidywalny, testowalny i łatwiejszy w utrzymaniu kod.

Czyste funkcje (Pure Functions)

Czysta funkcja to funkcja, która spełnia dwa warunki:

  1. Determinizm - dla tych samych argumentów zawsze zwraca ten sam wynik
  2. Brak efektów ubocznych - nie modyfikuje zewnętrznego stanu i nie wykonuje operacji I/O

Przykłady nieczystych funkcji

1// ❌ Nieczysta funkcja - zależy od zewnętrznego stanu
2let tax = 0.23;
3
4function calculatePrice(basePrice) {
5  return basePrice * (1 + tax); // Zależy od zewnętrznej zmiennej
6}
7
8// ❌ Nieczysta funkcja - modyfikuje zewnętrzny stan
9let counter = 0;
10
11function incrementCounter() {
12  counter++; // Efekt uboczny - modyfikacja globalnej zmiennej
13  return counter;
14}
15
16// ❌ Nieczysta funkcja - efekt uboczny (I/O)
17function greetUser(name) {
18  console.log(`Witaj, ${name}!`); // Efekt uboczny - output do konsoli
19  return name;
20}
21
22// ❌ Nieczysta funkcja - niedeterminizm
23function getCurrentTimestamp() {
24  return Date.now(); // Różne wyniki dla każdego wywołania
25}
26
27// ❌ Nieczysta funkcja - modyfikuje argument
28function addItem(array, item) {
29  array.push(item); // Mutuje przekazany argument
30  return array;
31}

Przykłady czystych funkcji

1// ✅ Czysta funkcja - determinizm i brak efektów ubocznych
2function calculatePrice(basePrice: number, taxRate: number): number {
3  return basePrice * (1 + taxRate);
4}
5
6// ✅ Czysta funkcja - operacja matematyczna
7function add(a: number, b: number): number {
8  return a + b;
9}
10
11// ✅ Czysta funkcja - transformacja danych
12function formatFullName(firstName: string, lastName: string): string {
13  return `${firstName} ${lastName}`;
14}
15
16// ✅ Czysta funkcja - praca z tablicami (bez mutacji)
17function addItem<T>(array: T[], item: T): T[] {
18  return [...array, item]; // Zwraca nową tablicę
19}
20
21// ✅ Czysta funkcja - filtrowanie
22function filterEvenNumbers(numbers: number[]): number[] {
23  return numbers.filter(num => num % 2 === 0);
24}
25
26// ✅ Czysta funkcja - przekształcenie obiektu
27interface User {
28  id: number;
29  name: string;
30  age: number;
31}
32
33function updateUserAge(user: User, newAge: number): User {
34  return {
35    ...user, // Spread operator - kopiowanie istniejących właściwości
36    age: newAge // Nadpisanie wieku
37  };
38}
39
40// ✅ Czysta funkcja - walidacja
41function isValidEmail(email: string): boolean {
42  const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
43  return emailRegex.test(email);
44}
45
46// Użycie czystych funkcji
47const products = [
48  { name: 'Laptop', basePrice: 2000 },
49  { name: 'Mouse', basePrice: 50 },
50  { name: 'Keyboard', basePrice: 150 }
51];
52
53const TAX_RATE = 0.23;
54
55const productsWithTax = products.map(product => ({
56  ...product,
57  finalPrice: calculatePrice(product.basePrice, TAX_RATE)
58}));
59
60console.log(productsWithTax);

Zalety czystych funkcji

1// Łatwość testowania - brak zależności zewnętrznych
2function calculateDiscount(price: number, discountPercent: number): number {
3  return price * (discountPercent / 100);
4}
5
6// Test jednostkowy jest prosty i niezawodny
7console.assert(calculateDiscount(100, 10) === 10);
8console.assert(calculateDiscount(200, 25) === 50);
9
10// Memoizacja - optymalizacja dzięki determinizmowi
11function memoize<TArgs extends any[], TReturn>(
12  fn: (...args: TArgs) => TReturn
13): (...args: TArgs) => TReturn {
14  const cache = new Map<string, TReturn>();
15  
16  return (...args: TArgs): TReturn => {
17    const key = JSON.stringify(args);
18    
19    if (cache.has(key)) {
20      console.log('Cache hit!');
21      return cache.get(key)!;
22    }
23    
24    console.log('Computing...');
25    const result = fn(...args);
26    cache.set(key, result);
27    return result;
28  };
29}
30
31// Funkcja kosztowna obliczeniowo
32function fibonacci(n: number): number {
33  if (n <= 1) return n;
34  return fibonacci(n - 1) + fibonacci(n - 2);
35}
36
37// Wersja z memoizacją
38const memoizedFibonacci = memoize(fibonacci);
39
40console.log(memoizedFibonacci(10)); // Computing... 55
41console.log(memoizedFibonacci(10)); // Cache hit! 55

Niemutowalność (Immutability)

Niemutowalność oznacza, że obiekty i struktury danych nie są modyfikowane po utworzeniu. Zamiast tego, tworzone są nowe wersje z wprowadzonymi zmianami.

Praca z prymitywami

1// Prymitywy w JavaScript są z natury niemutowalne
2let name = 'Jan';
3let newName = name.toUpperCase(); // Tworzy nowy string
4console.log(name);    // 'Jan' - oryginał nie zmieniony
5console.log(newName); // 'JAN' - nowa wartość

Praca z tablicami

1// ❌ Mutowalne operacje (modyfikują oryginalną tablicę)
2const originalArray = [1, 2, 3];
3originalArray.push(4);        // Mutuje
4originalArray.pop();          // Mutuje
5originalArray.splice(1, 1);   // Mutuje
6originalArray.sort();         // Mutuje
7
8// ✅ Niemutowalne operacje (tworzą nowe tablice)
9const numbers = [1, 2, 3];
10
11// Dodawanie elementów
12const withNewElement = [...numbers, 4];
13const withElementAtStart = [0, ...numbers];
14const withElementInMiddle = [...numbers.slice(0, 2), 2.5, ...numbers.slice(2)];
15
16// Usuwanie elementów
17const withoutFirst = numbers.slice(1);
18const withoutLast = numbers.slice(0, -1);
19const withoutMiddle = numbers.filter((_, index) => index !== 1);
20
21// Modyfikacja elementów
22const doubled = numbers.map(x => x * 2);
23const incremented = numbers.map(x => x + 1);
24
25// Sortowanie
26const sorted = [...numbers].sort();
27const reverseSorted = [...numbers].sort((a, b) => b - a);
28
29console.log('Oryginalna tablica:', numbers); // [1, 2, 3]
30console.log('Podwojone:', doubled);          // [2, 4, 6]
31console.log('Posortowane:', sorted);         // [1, 2, 3]
32
33// Zaawansowane operacje niemutowalne
34function updateArrayElement<T>(
35  array: T[], 
36  index: number, 
37  newValue: T
38): T[] {
39  return array.map((item, i) => i === index ? newValue : item);
40}
41
42function moveArrayElement<T>(
43  array: T[], 
44  fromIndex: number, 
45  toIndex: number
46): T[] {
47  const result = [...array];
48  const [removed] = result.splice(fromIndex, 1);
49  result.splice(toIndex, 0, removed);
50  return result;
51}
52
53const fruits = ['apple', 'banana', 'cherry'];
54const updatedFruits = updateArrayElement(fruits, 1, 'blueberry');
55const movedFruits = moveArrayElement(fruits, 0, 2);
56
57console.log('Oryginalne:', fruits);     // ['apple', 'banana', 'cherry']
58console.log('Zaktualizowane:', updatedFruits); // ['apple', 'blueberry', 'cherry']
59console.log('Przesunięte:', movedFruits);      // ['banana', 'cherry', 'apple']

Praca z obiektami

1// ❌ Mutowalne operacje na obiektach
2const user = { id: 1, name: 'Jan', age: 30 };
3user.age = 31;           // Mutuje
4user.email = 'jan@example.com'; // Mutuje
5delete user.name;        // Mutuje
6
7// ✅ Niemutowalne operacje na obiektach
8const originalUser = { id: 1, name: 'Jan', age: 30 };
9
10// Aktualizacja właściwości
11const olderUser = { ...originalUser, age: 31 };
12
13// Dodawanie właściwości
14const userWithEmail = { ...originalUser, email: 'jan@example.com' };
15
16// Usuwanie właściwości
17const { name, ...userWithoutName } = originalUser;
18
19// Zagnieżdżone aktualizacje
20interface Address {
21  street: string;
22  city: string;
23  country: string;
24}
25
26interface UserWithAddress {
27  id: number;
28  name: string;
29  address: Address;
30}
31
32const userWithAddress: UserWithAddress = {
33  id: 1,
34  name: 'Jan',
35  address: {
36    street: 'Main St',
37    city: 'Warsaw',
38    country: 'Poland'
39  }
40};
41
42// ❌ Mutowalna aktualizacja zagnieżdżona
43userWithAddress.address.city = 'Krakow'; // Mutuje
44
45// ✅ Niemutowalna aktualizacja zagnieżdżona
46const userInKrakow = {
47  ...userWithAddress,
48  address: {
49    ...userWithAddress.address,
50    city: 'Krakow'
51  }
52};
53
54// Helper functions dla niemutowalnych operacji
55function updateProperty<T, K extends keyof T>(
56  obj: T, 
57  key: K, 
58  value: T[K]
59): T {
60  return { ...obj, [key]: value };
61}
62
63function updateNestedProperty<T>(
64  obj: T, 
65  path: string[], 
66  value: any
67): T {
68  if (path.length === 1) {
69    return { ...obj, [path[0]]: value };
70  }
71  
72  const [firstKey, ...restPath] = path;
73  return {
74    ...obj,
75    [firstKey]: updateNestedProperty(obj[firstKey as keyof T], restPath, value)
76  };
77}
78
79// Użycie helper functions
80const updatedUser = updateProperty(originalUser, 'age', 35);
81const userInGdansk = updateNestedProperty(
82  userWithAddress, 
83  ['address', 'city'], 
84  'Gdansk'
85);
86
87console.log('Oryginał:', originalUser);     // { id: 1, name: 'Jan', age: 30 }
88console.log('Zaktualizowany:', updatedUser); // { id: 1, name: 'Jan', age: 35 }

Biblioteki do niemutowalności

1// Immer - biblioteka ułatwiająca pracę z niemutowalnymi strukturami
2// npm install immer
3
4import { produce } from 'immer';
5
6interface Todo {
7  id: number;
8  text: string;
9  completed: boolean;
10}
11
12const todos: Todo[] = [
13  { id: 1, text: 'Nauka JavaScript', completed: false },
14  { id: 2, text: 'Nauka TypeScript', completed: true },
15  { id: 3, text: 'Nauka React', completed: false }
16];
17
18// ✅ Z Immer - wygląda jak mutacja, ale tworzy nowy stan
19const updatedTodos = produce(todos, draft => {
20  const todo = draft.find(t => t.id === 1);
21  if (todo) {
22    todo.completed = true;
23  }
24});
25
26const todosWithNewItem = produce(todos, draft => {
27  draft.push({
28    id: 4,
29    text: 'Nauka programowania funkcyjnego',
30    completed: false
31  });
32});
33
34console.log('Oryginalne todos:', todos);
35console.log('Zaktualizowane todos:', updatedTodos);
36console.log('Todos z nowym elementem:', todosWithNewItem);
37
38// Zaawansowany przykład z zagnieżdżonymi strukturami
39interface Project {
40  id: number;
41  name: string;
42  todos: Todo[];
43  members: string[];
44}
45
46const project: Project = {
47  id: 1,
48  name: 'Projekt A',
49  todos: todos,
50  members: ['Jan', 'Anna']
51};
52
53const updatedProject = produce(project, draft => {
54  // Dodanie nowego członka
55  draft.members.push('Piotr');
56  
57  // Oznaczenie wszystkich todos jako zakończone
58  draft.todos.forEach(todo => {
59    todo.completed = true;
60  });
61  
62  // Zmiana nazwy projektu
63  draft.name = 'Projekt A - Zakończony';
64});
65
66console.log('Oryginalny projekt:', project);
67console.log('Zaktualizowany projekt:', updatedProject);

Currying (Karrowanie)

Currying to technika przekształcania funkcji przyjmującej wiele argumentów w serię funkcji, z których każda przyjmuje jeden argument.

Podstawowy currying

1// ❌ Funkcja z wieloma argumentami
2function multiply(a: number, b: number, c: number): number {
3  return a * b * c;
4}
5
6console.log(multiply(2, 3, 4)); // 24
7
8// ✅ Wersja curry
9function curriedMultiply(a: number) {
10  return function(b: number) {
11    return function(c: number) {
12      return a * b * c;
13    };
14  };
15}
16
17console.log(curriedMultiply(2)(3)(4)); // 24
18
19// ✅ Wersja curry z arrow functions
20const curriedMultiplyArrow = (a: number) => (b: number) => (c: number) => a * b * c;
21
22console.log(curriedMultiplyArrow(2)(3)(4)); // 24
23
24// Częściowe aplikowanie (partial application)
25const multiplyBy2 = curriedMultiply(2);
26const multiplyBy2And3 = multiplyBy2(3);
27
28console.log(multiplyBy2And3(4)); // 24
29console.log(multiplyBy2And3(5)); // 30

Praktyczne zastosowania currying

1// System logowania z currying
2type LogLevel = 'info' | 'warn' | 'error';
3
4const logger = (level: LogLevel) => (message: string) => (context?: object) => {
5  const timestamp = new Date().toISOString();
6  const logEntry = {
7    level,
8    message,
9    timestamp,
10    context
11  };
12  
13  console.log(JSON.stringify(logEntry, null, 2));
14};
15
16// Tworzenie specjalizowanych loggerów
17const infoLogger = logger('info');
18const errorLogger = logger('error');
19const warnLogger = logger('warn');
20
21// Użycie
22infoLogger('Aplikacja została uruchomiona')();
23errorLogger('Błąd połączenia z bazą danych')({ database: 'PostgreSQL' });
24warnLogger('Wysoka pamięć RAM')({ usage: '85%' });
25
26// System walidacji z currying
27type Validator<T> = (value: T) => boolean;
28
29const createValidator = <T>(predicate: (value: T) => boolean) => 
30  (errorMessage: string) => 
31  (value: T): { isValid: boolean; error?: string } => {
32    const isValid = predicate(value);
33    return isValid ? { isValid } : { isValid, error: errorMessage };
34  };
35
36// Predykaty walidacji
37const isNotEmpty = (value: string) => value.trim().length > 0;
38const isEmail = (value: string) => /^[^s@]+@[^s@]+.[^s@]+$/.test(value);
39const isMinLength = (minLength: number) => (value: string) => value.length >= minLength;
40const isNumber = (value: string) => !isNaN(Number(value));
41
42// Tworzenie walidatorów
43const requiredValidator = createValidator(isNotEmpty)('Pole jest wymagane');
44const emailValidator = createValidator(isEmail)('Nieprawidłowy format email');
45const passwordValidator = createValidator(isMinLength(8))('Hasło musi mieć co najmniej 8 znaków');
46const ageValidator = createValidator(isNumber)('Wiek musi być liczbą');
47
48// Użycie walidatorów
49console.log(requiredValidator('Jan'));        // { isValid: true }
50console.log(requiredValidator(''));           // { isValid: false, error: 'Pole jest wymagane' }
51console.log(emailValidator('jan@example.com')); // { isValid: true }
52console.log(passwordValidator('123'));         // { isValid: false, error: 'Hasło musi mieć co najmniej 8 znaków' }
53
54// System konfiguracji API z currying
55const apiCall = (baseUrl: string) => 
56  (endpoint: string) => 
57  (method: 'GET' | 'POST' | 'PUT' | 'DELETE') => 
58  (data?: any) => {
59    const config = {
60      url: `${baseUrl}${endpoint}`,
61      method,
62      data
63    };
64    
65    console.log('API Call:', config);
66    // Tutaj byłby rzeczywisty call do API
67    return Promise.resolve({ success: true, config });
68  };
69
70// Konfiguracja dla różnych środowisk
71const devApi = apiCall('https://dev-api.example.com');
72const prodApi = apiCall('https://api.example.com');
73
74// Specjalizowane funkcje dla różnych endpoints
75const usersApi = devApi('/users');
76const productsApi = devApi('/products');
77
78// Konkretne operacje
79const getUsers = usersApi('GET');
80const createUser = usersApi('POST');
81const updateUser = usersApi('PUT');
82
83// Użycie
84getUsers();
85createUser({ name: 'Jan', email: 'jan@example.com' });
86updateUser({ id: 1, name: 'Jan Kowalski' });

Automatyczne currying

1// Helper function do automatycznego currying
2function curry<TArgs extends any[], TReturn>(
3  fn: (...args: TArgs) => TReturn
4): (...args: any[]) => any {
5  return function curried(...args: any[]) {
6    if (args.length >= fn.length) {
7      return fn(...args as TArgs);
8    } else {
9      return function(...nextArgs: any[]) {
10        return curried(...args, ...nextArgs);
11      };
12    }
13  };
14}
15
16// Przykład użycia automatycznego currying
17function sum(a: number, b: number, c: number, d: number): number {
18  return a + b + c + d;
19}
20
21const curriedSum = curry(sum);
22
23// Różne sposoby wywołania
24console.log(curriedSum(1, 2, 3, 4));     // 10
25console.log(curriedSum(1)(2)(3)(4));     // 10
26console.log(curriedSum(1, 2)(3, 4));     // 10
27console.log(curriedSum(1)(2, 3)(4));     // 10
28
29// Częściowe aplikowanie
30const add5 = curriedSum(5);
31const add5And10 = add5(10);
32const add5And10And15 = add5And10(15);
33
34console.log(add5And10And15(20)); // 50
35
36// Przykład z filtrowaniem i sortowaniem
37function filterAndSort<T>(
38  predicate: (item: T) => boolean,
39  compareKey: keyof T,
40  array: T[]
41): T[] {
42  return array
43    .filter(predicate)
44    .sort((a, b) => {
45      if (a[compareKey] < b[compareKey]) return -1;
46      if (a[compareKey] > b[compareKey]) return 1;
47      return 0;
48    });
49}
50
51const curriedFilterAndSort = curry(filterAndSort);
52
53interface Product {
54  id: number;
55  name: string;
56  price: number;
57  category: string;
58  inStock: boolean;
59}
60
61const products: Product[] = [
62  { id: 1, name: 'Laptop', price: 2000, category: 'Electronics', inStock: true },
63  { id: 2, name: 'Mouse', price: 50, category: 'Electronics', inStock: false },
64  { id: 3, name: 'Book', price: 25, category: 'Books', inStock: true },
65  { id: 4, name: 'Keyboard', price: 150, category: 'Electronics', inStock: true }
66];
67
68// Specjalizowane funkcje
69const filterInStockSortByPrice = curriedFilterAndSort(
70  (product: Product) => product.inStock
71)('price');
72
73const filterElectronicsSortByName = curriedFilterAndSort(
74  (product: Product) => product.category === 'Electronics'
75)('name');
76
77console.log('Dostępne produkty sorted by price:', filterInStockSortByPrice(products));
78console.log('Electronics sorted by name:', filterElectronicsSortByName(products));

Kompozycja funkcji (Function Composition)

Kompozycja funkcji to technika łączenia prostych funkcji w bardziej złożone przez przekazywanie wyniku jednej funkcji jako argumentu do kolejnej.

Podstawowa kompozycja

1// Proste funkcje
2const add = (x: number) => x + 1;
3const multiply = (x: number) => x * 2;
4const square = (x: number) => x * x;
5
6// ❌ Bez kompozycji - zagnieżdżone wywołania
7const result1 = square(multiply(add(5))); // ((5 + 1) * 2)^2 = 144
8
9// ✅ Funkcja compose - wykonuje funkcje od prawej do lewej
10function compose<T>(...functions: Array<(arg: T) => T>) {
11  return (value: T): T => {
12    return functions.reduceRight((acc, fn) => fn(acc), value);
13  };
14}
15
16// ✅ Funkcja pipe - wykonuje funkcje od lewej do prawej (bardziej intuicyjna)
17function pipe<T>(...functions: Array<(arg: T) => T>) {
18  return (value: T): T => {
19    return functions.reduce((acc, fn) => fn(acc), value);
20  };
21}
22
23// Użycie compose
24const composedFunction = compose(square, multiply, add);
25console.log(composedFunction(5)); // 144
26
27// Użycie pipe (bardziej czytelne)
28const pipedFunction = pipe(add, multiply, square);
29console.log(pipedFunction(5)); // 144
30
31// Praktyczny przykład - przetwarzanie danych użytkownika
32interface User {
33  firstName: string;
34  lastName: string;
35  age: number;
36  email: string;
37}
38
39const users: User[] = [
40  { firstName: 'Jan', lastName: 'Kowalski', age: 25, email: 'jan@example.com' },
41  { firstName: 'Anna', lastName: 'Nowak', age: 30, email: 'anna@example.com' },
42  { firstName: 'Piotr', lastName: 'Wiśniewski', age: 22, email: 'piotr@example.com' }
43];
44
45// Funkcje pomocnicze
46const filterAdults = (users: User[]) => users.filter(user => user.age >= 18);
47const sortByAge = (users: User[]) => [...users].sort((a, b) => a.age - b.age);
48const mapToFullNames = (users: User[]) => 
49  users.map(user => `${user.firstName} ${user.lastName}`);
50
51// Kompozycja
52const processUsers = pipe(
53  filterAdults,
54  sortByAge,
55  mapToFullNames
56);
57
58const processedUsers = processUsers(users);
59console.log(processedUsers); // ['Piotr Wiśniewski', 'Jan Kowalski', 'Anna Nowak']

Zaawansowana kompozycja z TypeScript

1// Typowana kompozycja dla różnych typów
2function compose2<A, B, C>(f: (b: B) => C, g: (a: A) => B) {
3  return (a: A): C => f(g(a));
4}
5
6function compose3<A, B, C, D>(
7  f: (c: C) => D, 
8  g: (b: B) => C, 
9  h: (a: A) => B
10) {
11  return (a: A): D => f(g(h(a)));
12}
13
14// Przykład użycia typowanej kompozycji
15const parseNumber = (str: string): number => parseFloat(str);
16const addTax = (price: number): number => price * 1.23;
17const formatCurrency = (amount: number): string => `${amount.toFixed(2)} PLN`;
18
19const processPrice = compose3(formatCurrency, addTax, parseNumber);
20
21console.log(processPrice('100')); // '123.00 PLN'
22
23// System walidacji z kompozycją
24type ValidationResult = {
25  isValid: boolean;
26  errors: string[];
27};
28
29type Validator<T> = (value: T) => ValidationResult;
30
31const combineValidators = <T>(...validators: Validator<T>[]): Validator<T> => {
32  return (value: T): ValidationResult => {
33    const results = validators.map(validator => validator(value));
34    
35    return {
36      isValid: results.every(result => result.isValid),
37      errors: results.flatMap(result => result.errors)
38    };
39  };
40};
41
42// Pojedyncze walidatory
43const isRequired: Validator<string> = (value: string) => ({
44  isValid: value.trim().length > 0,
45  errors: value.trim().length === 0 ? ['Pole jest wymagane'] : []
46});
47
48const isEmail: Validator<string> = (value: string) => {
49  const isValid = /^[^s@]+@[^s@]+.[^s@]+$/.test(value);
50  return {
51    isValid,
52    errors: isValid ? [] : ['Nieprawidłowy format email']
53  };
54};
55
56const minLength = (min: number): Validator<string> => (value: string) => {
57  const isValid = value.length >= min;
58  return {
59    isValid,
60    errors: isValid ? [] : [`Minimum ${min} znaków wymagane`]
61  };
62};
63
64// Kompozycja walidatorów
65const emailValidator = combineValidators(isRequired, isEmail);
66const passwordValidator = combineValidators(isRequired, minLength(8));
67
68console.log(emailValidator('jan@example.com')); // { isValid: true, errors: [] }
69console.log(emailValidator(''));                // { isValid: false, errors: ['Pole jest wymagane'] }
70console.log(passwordValidator('123'));          // { isValid: false, errors: ['Minimum 8 znaków wymagane'] }
71
72// Przetwarzanie danych z kompozycją
73interface RawProduct {
74  name: string;
75  price: string;
76  description: string;
77  tags: string;
78}
79
80interface ProcessedProduct {
81  name: string;
82  price: number;
83  description: string;
84  tags: string[];
85  slug: string;
86}
87
88// Funkcje transformacji
89const parsePrice = (product: RawProduct): ProcessedProduct => ({
90  ...product,
91  price: parseFloat(product.price),
92  tags: [],
93  slug: ''
94});
95
96const parseTags = (product: ProcessedProduct): ProcessedProduct => ({
97  ...product,
98  tags: product.tags ? product.tags.split(',').map(tag => tag.trim()) : []
99});
100
101const generateSlug = (product: ProcessedProduct): ProcessedProduct => ({
102  ...product,
103  slug: product.name.toLowerCase().replace(/s+/g, '-').replace(/[^a-z0-9-]/g, '')
104});
105
106const validateProduct = (product: ProcessedProduct): ProcessedProduct => {
107  if (product.price <= 0) {
108    throw new Error('Cena musi być większa od 0');
109  }
110  if (!product.name.trim()) {
111    throw new Error('Nazwa produktu jest wymagana');
112  }
113  return product;
114};
115
116// Kompozycja przetwarzania
117const processProduct = pipe(
118  parsePrice,
119  parseTags,
120  generateSlug,
121  validateProduct
122);
123
124const rawProduct: RawProduct = {
125  name: 'Super Laptop',
126  price: '2999.99',
127  description: 'Najlepszy laptop na rynku',
128  tags: 'laptop, komputer, elektronika'
129};
130
131try {
132  const processed = processProduct(rawProduct);
133  console.log('Przetworzony produkt:', processed);
134} catch (error) {
135  console.error('Błąd przetwarzania:', error.message);
136}

Kompozycja asynchroniczna

1// Kompozycja dla funkcji asynchronicznych
2const composeAsync = <T>(...functions: Array<(arg: T) => Promise<T>>) => {
3  return (value: T): Promise<T> => {
4    return functions.reduceRight(
5      (acc, fn) => acc.then(fn),
6      Promise.resolve(value)
7    );
8  };
9};
10
11const pipeAsync = <T>(...functions: Array<(arg: T) => Promise<T>>) => {
12  return (value: T): Promise<T> => {
13    return functions.reduce(
14      (acc, fn) => acc.then(fn),
15      Promise.resolve(value)
16    );
17  };
18};
19
20// Przykład użycia z API
21interface ApiUser {
22  id: number;
23  name: string;
24  email: string;
25}
26
27interface EnrichedUser extends ApiUser {
28  avatar: string;
29  lastLogin: Date;
30  preferences: any;
31}
32
33// Funkcje asynchroniczne
34const fetchUser = async (userId: number): Promise<ApiUser> => {
35  console.log(`Fetching user ${userId}`);
36  // Symulacja API call
37  return new Promise(resolve => {
38    setTimeout(() => {
39      resolve({
40        id: userId,
41        name: 'Jan Kowalski',
42        email: 'jan@example.com'
43      });
44    }, 100);
45  });
46};
47
48const enrichWithAvatar = async (user: ApiUser): Promise<EnrichedUser> => {
49  console.log('Enriching with avatar');
50  return new Promise(resolve => {
51    setTimeout(() => {
52      resolve({
53        ...user,
54        avatar: `https://avatars.example.com/${user.id}`,
55        lastLogin: new Date(),
56        preferences: {}
57      });
58    }, 50);
59  });
60};
61
62const enrichWithPreferences = async (user: EnrichedUser): Promise<EnrichedUser> => {
63  console.log('Enriching with preferences');
64  return new Promise(resolve => {
65    setTimeout(() => {
66      resolve({
67        ...user,
68        preferences: {
69          theme: 'dark',
70          language: 'pl',
71          notifications: true
72        }
73      });
74    }, 50);
75  });
76};
77
78// Kompozycja asynchroniczna
79const enrichUser = pipeAsync(
80  enrichWithAvatar,
81  enrichWithPreferences
82);
83
84// Użycie
85async function main() {
86  try {
87    const user = await fetchUser(1);
88    const enrichedUser = await enrichUser(user);
89    console.log('Wzbogacony użytkownik:', enrichedUser);
90  } catch (error) {
91    console.error('Błąd:', error);
92  }
93}
94
95main();

Praktyczny przykład - System e-commerce z programowaniem funkcyjnym

1// Kompleksowy przykład zastosowania programowania funkcyjnego
2
3// Typy danych
4interface Product {
5  id: string;
6  name: string;
7  price: number;
8  category: string;
9  tags: string[];
10  inStock: boolean;
11  rating: number;
12}
13
14interface CartItem {
15  product: Product;
16  quantity: number;
17}
18
19interface Cart {
20  items: CartItem[];
21  discountCode?: string;
22}
23
24interface Order {
25  id: string;
26  items: CartItem[];
27  subtotal: number;
28  discount: number;
29  tax: number;
30  total: number;
31  timestamp: Date;
32}
33
34// Czyste funkcje dla produktów
35const filterByCategory = (category: string) => (products: Product[]) =>
36  products.filter(product => product.category === category);
37
38const filterByPriceRange = (min: number, max: number) => (products: Product[]) =>
39  products.filter(product => product.price >= min && product.price <= max);
40
41const filterInStock = (products: Product[]) =>
42  products.filter(product => product.inStock);
43
44const sortByPrice = (ascending: boolean = true) => (products: Product[]) =>
45  [...products].sort((a, b) => ascending ? a.price - b.price : b.price - a.price);
46
47const sortByRating = (products: Product[]) =>
48  [...products].sort((a, b) => b.rating - a.rating);
49
50// Funkcje dla koszyka
51const addToCart = (product: Product, quantity: number) => (cart: Cart): Cart => {
52  const existingItemIndex = cart.items.findIndex(item => item.product.id === product.id);
53  
54  if (existingItemIndex >= 0) {
55    return {
56      ...cart,
57      items: cart.items.map((item, index) =>
58        index === existingItemIndex
59          ? { ...item, quantity: item.quantity + quantity }
60          : item
61      )
62    };
63  }
64  
65  return {
66    ...cart,
67    items: [...cart.items, { product, quantity }]
68  };
69};
70
71const removeFromCart = (productId: string) => (cart: Cart): Cart => ({
72  ...cart,
73  items: cart.items.filter(item => item.product.id !== productId)
74});
75
76const updateQuantity = (productId: string, newQuantity: number) => (cart: Cart): Cart => {
77  if (newQuantity <= 0) {
78    return removeFromCart(productId)(cart);
79  }
80  
81  return {
82    ...cart,
83    items: cart.items.map(item =>
84      item.product.id === productId
85        ? { ...item, quantity: newQuantity }
86        : item
87    )
88  };
89};
90
91const applyDiscountCode = (discountCode: string) => (cart: Cart): Cart => ({
92  ...cart,
93  discountCode
94});
95
96// Funkcje do obliczania cen
97const calculateItemTotal = (item: CartItem): number =>
98  item.product.price * item.quantity;
99
100const calculateSubtotal = (items: CartItem[]): number =>
101  items.reduce((total, item) => total + calculateItemTotal(item), 0);
102
103const calculateDiscount = (subtotal: number, discountCode?: string): number => {
104  const discountRates: Record<string, number> = {
105    'SAVE10': 0.1,
106    'SAVE20': 0.2,
107    'STUDENT': 0.15
108  };
109  
110  if (!discountCode || !discountRates[discountCode]) {
111    return 0;
112  }
113  
114  return subtotal * discountRates[discountCode];
115};
116
117const calculateTax = (amount: number, taxRate: number = 0.23): number =>
118  amount * taxRate;
119
120const calculateTotal = (subtotal: number, discount: number, tax: number): number =>
121  subtotal - discount + tax;
122
123// Kompozycja dla przetwarzania zamówienia
124const processOrder = (cart: Cart): Order => {
125  const subtotal = calculateSubtotal(cart.items);
126  const discount = calculateDiscount(subtotal, cart.discountCode);
127  const taxableAmount = subtotal - discount;
128  const tax = calculateTax(taxableAmount);
129  const total = calculateTotal(subtotal, discount, tax);
130  
131  return {
132    id: 'order_' + Math.random().toString(36).substr(2, 9),
133    items: cart.items,
134    subtotal,
135    discount,
136    tax,
137    total,
138    timestamp: new Date()
139  };
140};
141
142// Funkcje wyszukiwania z kompozycją
143const searchProducts = (
144  query: string,
145  category?: string,
146  priceRange?: { min: number; max: number },
147  sortBy: 'price' | 'rating' = 'rating'
148) => (products: Product[]): Product[] => {
149  const searchByName = (products: Product[]) =>
150    products.filter(product =>
151      product.name.toLowerCase().includes(query.toLowerCase()) ||
152      product.tags.some(tag => tag.toLowerCase().includes(query.toLowerCase()))
153    );
154  
155  const filters = [
156    searchByName,
157    filterInStock
158  ];
159  
160  if (category) {
161    filters.push(filterByCategory(category));
162  }
163  
164  if (priceRange) {
165    filters.push(filterByPriceRange(priceRange.min, priceRange.max));
166  }
167  
168  const sorter = sortBy === 'price' ? sortByPrice() : sortByRating;
169  
170  return pipe(...filters, sorter)(products);
171};
172
173// Przykład użycia
174const products: Product[] = [
175  {
176    id: '1',
177    name: 'Laptop Gaming',
178    price: 3500,
179    category: 'electronics',
180    tags: ['gaming', 'laptop', 'high-performance'],
181    inStock: true,
182    rating: 4.8
183  },
184  {
185    id: '2',
186    name: 'Mysz Gamingowa',
187    price: 150,
188    category: 'electronics',
189    tags: ['gaming', 'mouse', 'rgb'],
190    inStock: true,
191    rating: 4.5
192  },
193  {
194    id: '3',
195    name: 'Książka JavaScript',
196    price: 89,
197    category: 'books',
198    tags: ['programming', 'javascript', 'learning'],
199    inStock: false,
200    rating: 4.9
201  }
202];
203
204// Tworzenie koszyka funkcyjnie
205const emptyCart: Cart = { items: [] };
206
207const myCart = pipe(
208  addToCart(products[0], 1),
209  addToCart(products[1], 2),
210  applyDiscountCode('SAVE10')
211)(emptyCart);
212
213console.log('Koszyk:', myCart);
214
215// Przetwarzanie zamówienia
216const order = processOrder(myCart);
217console.log('Zamówienie:', order);
218
219// Wyszukiwanie produktów
220const searchGaming = searchProducts('gaming', 'electronics', undefined, 'price');
221const gamingProducts = searchGaming(products);
222console.log('Produkty gaming:', gamingProducts);
223
224// Curried funkcje dla częstego użycia
225const addLaptopToCart = addToCart(products[0]);
226const addMouseToCart = addToCart(products[1]);
227
228const cartWithLaptop = addLaptopToCart(1)(emptyCart);
229const cartWithBoth = addMouseToCart(2)(cartWithLaptop);
230
231console.log('Koszyk z laptopem i myszką:', cartWithBoth);

Podsumowanie programowania funkcyjnego

Programowanie funkcyjne w JavaScript/TypeScript oferuje:

  1. Czyste funkcje - przewidywalność i łatwość testowania
  2. Niemutowalność - bezpieczeństwo i brak efektów ubocznych
  3. Currying - elastyczność i ponowne wykorzystanie
  4. Kompozycja - budowanie złożonej logiki z prostych elementów

Zalety:

  • Kod jest bardziej przewidywalny i łatwiejszy do zrozumienia
  • Łatwiejsze testowanie jednostkowe
  • Lepsza możliwość równoległego przetwarzania
  • Zmniejszona liczba bugów związanych z mutacją stanu

Kiedy stosować:

  • Przetwarzanie danych i transformacje
  • Systemy z dużą ilością obliczeń
  • Aplikacje wymagające wysokiej niezawodności
  • Kod wymagający częstego testowania

Podobnie jak matematycy tworzą eleganckie dowody łącząc proste aksjomaty, programowanie funkcyjne pozwala budować złożone systemy z prostych, niezawodnych elementów.

Ir a CodeWorlds