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.
Czysta funkcja to funkcja, która spełnia dwa warunki:
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}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);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! 55Niemutowalność oznacza, że obiekty i struktury danych nie są modyfikowane po utworzeniu. Zamiast tego, tworzone są nowe wersje z wprowadzonymi zmianami.
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ść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']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 }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 to technika przekształcania funkcji przyjmującej wiele argumentów w serię funkcji, z których każda przyjmuje jeden argument.
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)); // 301// 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' });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 to technika łączenia prostych funkcji w bardziej złożone przez przekazywanie wyniku jednej funkcji jako argumentu do kolejnej.
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']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}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();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);Programowanie funkcyjne w JavaScript/TypeScript oferuje:
Zalety:
Kiedy stosować:
Podobnie jak matematycy tworzą eleganckie dowody łącząc proste aksjomaty, programowanie funkcyjne pozwala budować złożone systemy z prostych, niezawodnych elementów.