FUNCTIONAL PROGRAMMING

Від теорії до практики

$ whoami

Lead Software Engineer

8 років із GlobalLogic

9 років у Web Development

 

Speaker, mentor та Trusted Interviewer в GlobalLogic

Частина програмного комітету Fwdays конференції (JavaScript/React/TS)

Інна Іващук

AGENDA

  1. Інтро в функціональне програмування
  2. Функціональне програмування в різних мовах та фреймворках
  3. Основні концепції
  4. Від теорії до практики
  5. Що таке монади та функтори?
  6. Quiz
  7. Q&A

Інтро в функціональне програмування

01

 Його зачатки можна знайти в роботах над лямбда-численням, розробленим американським математиком 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

Чому функціональне програмування стало популярним?

Тісний зв'язок з математикою забезпечує строгу семантику і спрощує доведення коректності програм

Чистота і передбачуваність

Високий рівень абстракції

Математична основа

Паралелізм і розподілені обчислення

Функціональні програми часто легше паралелізувати, що важливо для сучасних багатоядерних процесорів

Функціональні мови дозволяють писати більш компактний і виразний код

Відсутність побічних ефектів робить код більш зрозумілим і легшим для тестування

Функціональне програмування в різних мовах і фреймворках

02

Чисті функціональні мови

строго типізована мова, компілюється в 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

Основні концепції

03

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)

Від теорії до практики

04

First-class function

Функції першого класу мають ті ж властивості, що й інші об'єкти: їх можна зберігати в змінних, передавати та повертати. 

 (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

Чиста функція завжди повертає один і той самий результат для однакових вхідних даних і не має побічних ефектів (не змінює зовнішній стан)

// 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;
}

Higher-order function (HOF)

Функція вищого порядку – це функція, яка або приймає інші функції як аргументи, або повертає функцію як результат

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

Function composition

Композиція функцій - це концепція в програмуванні, яка передбачає об'єднання двох або більше функцій в одну, де результат однієї функції стає вхідними даними для наступної

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)));

Immutability

Імутабельність - це концепція, за якою дані після створення не повинні бути зміненими, а замість зміни існуючих даних, ми створюємо нові

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);

Recursion

Рекурсія - це техніка, коли функція викликає сама себе

function factorial(n) {
  if (n === 0) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

console.log(factorial(5)); // 120

Currying

Каррінг - це техніка, при якій функція з кількома аргументами перетворюється на послідовність функцій, кожна з яких приймає один аргумент

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 кількістю аргументів

Lazy Evaluation

Ліниві обчислення — це коли значення не обчислюється до тих пір, поки воно дійсно не потрібне

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 не є "принципами", як чисті функції, але вони є важливими інструментами функціонального програмування

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()

І звичиайно, що не можна просто так взяти

 і не згадати про монади і функтори

Що таке монади та функтори

05

Функтор та монад – це концепції, запозичені з теорії категорій, які знайшли широке застосування в функціональному програмуванні

Functor

Функтор – це об'єкт із методом 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

 

Monad

Монад - це абстракція в 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 як монади

І підсумуємо всі ці концепції FP

Знаю ООР, FPP, алгоритми і структури даних, патерни проектування і навіть більше

Знаю щось про ООР і про singleton

Quiz

Q&A

Корисні

ресурси

1) Functional programming in JavaScript

Fun fun function YouTube playlist

 

2) Стаття на DOU (переклад)

Функтори, Аплікативи, та Монади з ілюстраціями

 

3) З прикладом використання в Scala

https://medium.com/beingprofessional/understanding-functor-and-monad

Дякую

FUNCTIONAL PROGRAMMING

Від теорії до практики

Loading...