Від теорії до практики
Lead Software Engineer
8 років із GlobalLogic
9 років у Web Development
Speaker, mentor та Trusted Interviewer в GlobalLogic
Частина програмного комітету Fwdays конференції (JavaScript/React/TS)
Інна Іващук
Його зачатки можна знайти в роботах над лямбда-численням, розробленим американським математиком Alonzo Church у 1930-х роках.
Функціональне програмування – це парадигма, коріння якої сягають глибоко в математику, а тому програми будуються шляхом застосування та компонування функцій.
Number
Function definition
Lambda expression
Перші застосування в програмуванні: Lisp (1958)
Одна з найстаріших мов програмування, яка була натхненна лямбда-численням і стала піонером у сфері функціонального програмування.
Lisp: знаходження n! з використанням рекурсії
(write-line "Please enter a number...")
(setq x (read))
(defun factorial(n)
(if (= n 1)
(setq a 1)
)
(if (> n 1)
(setq a (* n (factorial (- n 1))))
)
(format t "~D! is ~D" n a)
a ;;
)
(factorial x)
Перші застосування в програмуванні: ML (1958)
Мова ML (Meta Language) була розроблена для формальної верифікації програм. Вона внесла значний вклад у розвиток статичної типізації та інференції типів у функціональних мовах
fun fact n = let
fun fac 0 acc = acc
| fac n acc = fac (n - 1) (n * acc)
in
if (n < 0) then raise Domain else fac n 1
end
Починаючи з 1990-х років, функціональні концепції почали активно інтегруватися в інші мови програмування, такі як C++, Java, Python і JavaScript
Tim Berns-Lee, 1990
Чому функціональне програмування стало популярним?
Тісний зв'язок з математикою забезпечує строгу семантику і спрощує доведення коректності програм
Чистота і передбачуваність
Високий рівень абстракції
Математична основа
Паралелізм і розподілені обчислення
Функціональні програми часто легше паралелізувати, що важливо для сучасних багатоядерних процесорів
Функціональні мови дозволяють писати більш компактний і виразний код
Відсутність побічних ефектів робить код більш зрозумілим і легшим для тестування
Чисті функціональні мови
строго типізована мова, компілюється в JavaScript (створено на Haskell)
для створення веб-інтерфейсів
а також F (F-star)*, Frege, Idris, Agda
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n-1)
main :: IO ()
main = do
let num = 5
print (factorial num)
Реалізація n! в Haskell
# factorial.pur
module Main where
import Prelude (show, ($), (=<<), (+), (<>), (*), (-))
import Control.Monad.Eff.Console(log)
import TryPureScript
fact :: Int -> Int
fact 0 = 1
fact n = n * fact(n-1)
main = render =<< withConsole do
log $ show $ fact 6
Реалізація n! в PureScript
Мови з сильною підтримкою функціонального програмування
F# від Microsoft та
працює на .NET платформі
діалект Lisp, який працює на JVM
поєднують в собі OOP та FP
;; Clojure
(defn fact
([n] (fact n 1))
([n f]
(if (<= n 1)
f
(recur (dec n) (* f n)))))
Реалізація n! в Clojure та Scala
// Scala
def factorial(n: Int): Int =
{
if (n == 0)
return 1
else
return n * factorial(n-1)
}
Мови з частковою підтримкою функціонального програмування
Функціональні парадигми є навіть в Java та C#
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubled = numbers
.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
List<int> doubled = numbers.Select(n => n * 2).ToList();
"JavaScript (JS) is a lightweight interpreted (or just-in-time compiled) programming language with first-class functions. JavaScript is a prototype-based, multi-paradigm, single-threaded, dynamic language, supporting object-oriented, imperative, and declarative (e.g. functional programming) styles."
MDN Web Docs
JavaScript фреймворки та бібліотеки, які використовують принципи функціонального програмування
Sanctuary
fp-ts
Ramda
Imperative
Парадигми програмування
Declarative
Structured
Procedural
Logical
Functional
це спосіб описувати програми, де ми фокусуємося на тому що програма має робити, а не на як вона це робить
Імперативний vs Декларативний
const numbers = [1, 2, 3, 4, 5];
const filteredNumbers = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] > 3) {
filteredNumbers.push(numbers[i]);
}
}
console.log(filteredNumbers); // [4, 5]
const numbers = [1, 2, 3, 4, 5];
const filteredNumbers = numbers
.filter(number => number > 3);
console.log(filteredNumbers); // [4, 5]
Імперативний vs Декларативний
// Bubble sort
const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
for (let i = 0; i < numbers.length - 1; i++) {
for (let j = 0; j < numbers.length - i - 1; j++) {
if (numbers[j] > numbers[j + 1]) {
// Міняємо місцями елементи
const temp = numbers[j];
numbers[j] = numbers[j + 1];
numbers[j + 1] = temp;
}
}
}
console.log(numbers);
const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
const sortedNumbers = numbers
.sort((a, b) => a - b);
console.log(sortedNumbers);
Композиція
функцій
Функції вищого порядку
Декларативний
стиль
Незмінність даних
Чисті
функції
Ледачі
обчислення
Рекурсії
Каррінг
Функції першого класу
(first-class functions)
Функції першого класу мають ті ж властивості, що й інші об'єкти: їх можна зберігати в змінних, передавати та повертати.
(first-class citizens or first-class objects)
Тобто функції можуть бути присвоєні змінним, передані як аргументи в інші функції, повернуті як результат з інших функцій та зберігатися в структурах даних (наприклад, масивах або об'єктах)
// First-class function
const greet = () => "Hello!";
const storeFunction = greet; // Saved in a variable
storeFunction(); // Call fn using that variable
// Stored in object
const myUtils = {
sayHi: greet,
sayBye: () => "Bye!",
}
myUtils.sayBye();
// Stored in array
const listOfFns = [greet, myUtils.sayBye];
listOfFns[0]();
Чиста функція завжди повертає один і той самий результат для однакових вхідних даних і не має побічних ефектів (не змінює зовнішній стан)
// Pure function
function add(a, b) {
return a + b;
}
// Function with side effects (not a pure fn)
let count = 0;
function increment() {
count++;
return count;
}
Функція вищого порядку – це функція, яка або приймає інші функції як аргументи, або повертає функцію як результат
const numbers = [1, 2, 3, 4, 5];
// map
const doubled = numbers.map(x => x * 2); // [2, 4, 6, 8, 10]
// flatMap
const arr = [[1, 2], [3, 4], [5]];
const flattened = arr.flatMap(subArr => subArr); // [1, 2, 3, 4, 5]
// filter
const evenNumbers = numbers.filter(x => x % 2 === 0); // [2, 4]
// reduce
const sum = numbers.reduce((total, x) => total + x, 0); // 15
// forEach
numbers.forEach(x => console.log(x)); // 1 2 3 4 5
// our HOF
function myAwesomeFilter(array, predicate) {
const result = [];
for (let i = 0; i < array.length; i++) {
if (predicate(array[i])) {
result.push(array[i]);
}
}
return result;
}
// given data
const arr = [2, 55, 33, 1, 100, 99];
// trigger our HOF
myAwesomeFilter(arr, (item) => item > 10);
// result [55, 33, 100, 99]
Ми можемо створювати і свої власні HOF
callback fn
Композиція функцій - це концепція в програмуванні, яка передбачає об'єднання двох або більше функцій в одну, де результат однієї функції стає вхідними даними для наступної
const double = x => x * 2;
const increment = x => x + 1;
// Example 1
const composedFunction = x => increment(double(x));
// Example 2
const numbers = [1, 2, 3, 4, 5];
const updated = numbers.map(x => increment(double(x)));
Імутабельність - це концепція, за якою дані після створення не повинні бути зміненими, а замість зміни існуючих даних, ми створюємо нові
const numbers = [1, 2, 3];
// Non immutable way
const doubledNumbers = numbers.map(number => number * 2);
// [1, 2, 3] - original array is the same
console.log(numbers);
// [2, 4, 6] - new array
console.log(doubledNumbers);
const users = [
{ name: 'Linus Torvalds', technology: 'Linux' },
{ name: 'James Gosling', technology: 'Java' },
{ name: 'Dennis Ritchie', technology: 'C' },
{ name: 'Tim Berners-Lee', technology: 'World Wide Web'},
{ name: 'Bjarne Stroustrup', technology: 'C++' },
{ name: 'Brendan Eich', technology: 'JavaScript' },
];
function transformUsers(usersData) {
return users.map((user, index) => {
const [firstName, lastName] = user.name.split(' ');
return {
name: user.name,
technology: user.technology,
firstName,
lastName,
}
});
}
transformUsers(users);
const moviesList = [{
title: 'The Lord of the Rings',
genre: 'fantasy',
direcror: 'Peter Jackson',
year: 2001,
}, {
title: 'Dune',
genre: 'Sci-Fi',
direcror: 'Denis Villeneuve',
year: 2021,
}];
// Fn 1: filter by year
function filterByYear(movies, year) {
return movies.filter(item => item.year === year);
}
filterByYear(moviesList, 2021);
// Fn 2: add field rating with value 10
function addRating(movies) {
movies.forEach(movie => {
movie.rating = 10;
});
return 'Rating field is added';
}
addRating(moviesList);
Рекурсія - це техніка, коли функція викликає сама себе
function factorial(n) {
if (n === 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
console.log(factorial(5)); // 120
Каррінг - це техніка, при якій функція з кількома аргументами перетворюється на послідовність функцій, кожна з яких приймає один аргумент
function sum(a) {
return (b) => {
return (c) => {
return a + b + c
}
}
}
console.log(sum(1)(2)(3)) // 6
function sum(a){
let result = a; //closure
return function inner(b){
if(!b) {
return result;
}
result += b; //step 3
return inner;
}
}
console.log(sum(1)(2)(3)(4)()) // 6
І трішки покращень, щоб функція працювала з n кількістю аргументів
Ліниві обчислення — це коли значення не обчислюється до тих пір, поки воно дійсно не потрібне
function* lazyRange(start, end) {
let current = start;
while (current <= end) {
yield current;
current++;
}
}
// Usages
const numbers = lazyRange(1, 5);
numbers.next().value; // 1 (evaluate only now)
numbers.next().value; // 2 (evaluate only now)
numbers.next().value; // 3 (and so on)
У JavaScript ліниві обчислення можна реалізувати використовуючи певні конструкції, такі як генератори, замикання або стрілочні-функції
Операції map і reduce не є "принципами", як чисті функції, але вони є важливими інструментами функціонального програмування
map()
reduce()
function makeSandwiches(ingredients) {
return ingredients
.reduce((acc, ingredient, index) => {
// Group every two ingredients into a single sandwich
if (index % 2 === 0) {
// Start a new sandwich with the first ingredient
acc.push([ingredient]);
} else {
// Add the second ingredient to the current sandwich
acc[acc.length - 1].push(ingredient);
}
return acc;
}, [])
.map(filling => ({
bread: 'ciabatta',
filling, // Add the grouped ingredients as filling
}));
}
// Example usage
const ingredients = ['ham', 'cheese', 'tomato', 'lettuce', 'turkey', 'avocado'];
const sandwiches = makeSandwiches(ingredients);
// result [{ bread: "ciabatta", filling: ['ham', 'cheese']}, ...]
const users = [
{ name: 'Linus Torvalds' },
{ name: 'James Gosling' },
{ name: 'Tim Berners-Lee' },
{ name: 'Brendan Eich' },
];
// using filter().map()
const updatedUsers = users
.filter((item) => item.name === 'Linus Torvalds')
.map((item, index) => ({ ...item, osCreator: true })); // O(2 * n)
// using only reduce
const updatedUsersR = users.reduce((acc, item, index) => {
if(item.name === 'Linus Torvalds'){
acc.push({
...item,
osCreator: true,
})
}
return acc;
}, []); // O(n)
Моє улюблене використання reduce()
І звичиайно, що не можна просто так взяти
і не згадати про монади і функтори
Функтор та монад – це концепції, запозичені з теорії категорій, які знайшли широке застосування в функціональному програмуванні
Функтор – це об'єкт із методом map(), який може бути застосований до іншого об'єкта, трансформуючи його.
Репрезентаці функторів в Haskell і Scala
# Haskell
class Functor f where
fmap :: (a -> b) -> f a -> f b
// Scala
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
А як на рахунок JavaScript?
const numbers = [1, 2, 3, 4];
// Make numbers better and increase every by 42
numbers.map(n => n + 42)
і callback, який трансформує дані
Об'єкт Array можна вважати функтором, так як він містить реалізацію методу map()
Правда є один нюанс
const numbers = [1, 2, 3, 4];
// Make numbers better and increase every by 42
numbers.map((n, inedx) => n + 42)
другий аргумент дещо порушує класичну реалізацію
Ramda
import R from 'ramda';
// Функція для глибокого копіювання,
// використовуючи функтор для обробки масивів та об'єктів
const deepClone = R.map(x => {
if (R.is(Object, x)) {
return deepClone(x);
} else if (R.is(Array, x)) {
return deepClone(x);
} else {
return x;
}
});
const originalObject = {
a: 1,
b: [2, 3],
c: { d: 4 }
};
const clonedObject = deepClone(originalObject);
console.log(clonedObject);
// result { a: 1, b: [ 2, 3 ], c: { d: 4 } }
"library with a functional flavor"
Ramda
Streams
import { Readable } from 'node:stream';
import { Resolver } from 'node:dns/promises';
// An asynchronous mapper, making at most 2 queries at a time.
const resolver = new Resolver();
const dnsResults = Readable.from([
'nodejs.org',
'openjsf.org',
'www.linuxfoundation.org',
])
.map((domain) => resolver.resolve4(domain), { concurrency: 2 });
for await (const result of dnsResults) {
// Logs the DNS result of resolver.resolve4.
console.log(result);
}
Також ми можемо створити свій функтор
const Functor = value => ({
map: fn => Functor(fn(value)),
chain: fn => fn(value),
of: () => value
});
// Usages
function randomIntWrapper(max) {
return Functor(max)
.map(n => n + 1) // max + 1
.map(n => n * Math.random()) // Math.random() * (max + 1)
.map(Math.floor) // Math.floor(Math.random() * (max + 1))
}
const randomNumber = randomIntWrapper(333)
randomNumber.of() // returns number between 0 and 333
randomNumber.chain(n => n - 1) // returns (number between 0 and 333) - 1
Монад - це абстракція в FP, яка дозволяє послідовно обробляти дані та керувати побічними ефектами (стан, винятки) у безпечний та організований спосіб
Монада - як доставка піци
orderPizza(ingredients)
.sendToKitchen(ingredients)
.wrapInABox();
Кожен наступний крок — це нова дія:
.addSauce()
.addCheese()
Монада дозволяє нам взаємодіяти з піцою (додавати соус чи сир), не відкриваючи коробки до кінця доставки
class Maybe {
constructor(value) {
this.value = value;
}
static of(value) {
return new Maybe(value);
}
flatMap(fn) {
// Skip computation if value is null or undefined
return this.value == null
? Maybe.of(null)
: fn(this.value);
}
map(fn) {
return this.value == null
? Maybe.of(null)
: Maybe.of(fn(this.value));
}
}
Давайте створимо свою монаду
// Some functions that might return undefined or null
// findUser imitates request to a server
const findUser = (id) => id === 1
? Maybe.of({ name: "Alice" })
: Maybe.of(null);
const getName = (user) => Maybe.of(user.name);
const result = Maybe.of(1) // id = 1
.flatMap(findUser) // Find the user
.flatMap(getName); // Get the user's name
console.log(result.value); // Outputs: "Alice"
// When no user found by provided ID
const noResult = Maybe.of(3)
.flatMap(findUser) // Find the user
.flatMap(getName); // Get the user's name
console.log(noResult.value); // Outputs: null
Тобто монад - це коли функції поводяться добре, навіть коли вони хочуть трохи похуліганити 😄
// Приклад 1
Promise.resolve(10)
.then(x => x + 5) // 15
.then(x => x * 2) // 30
.then(console.log); // Output: 30
// Приклад 2
const fetchUser = userId =>
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.catch(error => Promise.reject(error));
fetchUser(123)
.then(getUserAddress)
.then(address => console.log(address))
.catch(error => console.error(error));
Promises як монади
Знаю ООР, FPP, алгоритми і структури даних, патерни проектування і навіть більше
Знаю щось про ООР і про singleton
1) Functional programming in JavaScript
Fun fun function YouTube playlist
2) Стаття на DOU (переклад)
Функтори, Аплікативи, та Монади з ілюстраціями
3) З прикладом використання в Scala
https://medium.com/beingprofessional/understanding-functor-and-monad
Від теорії до практики
Loading...