Функциональное программирование с использованием библиотеки fp-ts | Odessa Frontend Meetup #19
Dec. 24, 2021•0 likes•212 views
Download to read offline
Report
Technology
Дмитрий Ховрич рассказывает как использовать строгую типизацию TypeScript и писать надёжный код в функциональном стиле. А также делится знаниями как использовать функторы и монады в ежедневной фронтенд разработке.
5. Зачем писать код в функциональном стиле?
Функциональное программирование, как и любая другая парадигма,
накладывает дополнительные ограничения и правила.
Соблюдение этих ограничений и правил позволяет писать более надежный
код, который легче тестировать и повторно использовать.
6. Как мы можем себя ограничить во время написания кода?
“Хард” ограничения
● Компилятор TypeScript
○ Максимально строгий режим
○ TypeScript: Раскладываем tsconfig по
полочкам. Часть 1
○ TypeScript: Раскладываем tsconfig по
полочкам. Часть 2 — Всё про строгость
● ESLint
“Софт” ограничения
● Парадигма программирования
● Паттерны и лучшие практики
● Код ревью
7. Библиотека fp-ts
Библиотека fp-ts позволяет писать чистые функциональные приложения,
построенные поверх высокоуровневых абстракций из таких языков, как
Haskell, PureScript, и Scala.
● Паттерны и надежные абстракции из функционального мира
● Утилиты для композиции функций
● Функциональные типы данных
● Классы типов (type classes)
8. Класс типов
Класс типов определяет множество типизированных функций и констант,
которые должны существовать для каждого типа, который принадлежит
данному классу.
Основаны на теории категорий
9. Теория категорий
Теория категорий — раздел математики, изучающий свойства отношений
между математическими объектами, не зависящие от внутренней структуры
объектов.
11. Теория категорий
Вам не нужно изучать теорию
категорий для того, чтобы
использовать
функциональный подход в
Ваших программах!
12. Класс типов Show
interface Show<A> {
readonly show: (a: A) => string;
}
Тип A принадлежит классу Show, если для A определена функция show : (a:
A) => string
13. Реализации класса типов Show
const showNumber: Show<number> = {
show: n => n.toString()
};
const showUser: Show<User> = {
show: user => `User "${user.name}", ${user.age} years old`
};
15. Из чего состоит функциональное программирование?
Концепции, которые
“лежат на поверхности”
Концепции, которые
“спрятаны поглубже”
16. Концепции, которые “лежат на поверхности”
● Иммутабельные данные
● Чистые функции (referentially transparent)
○ Каррирование
● Побочные эффекты
● “Честные” сигнатуры функций (method signature honesty)
● Композиция функций (бесточечный стиль)
17. Иммутабельные данные
● Изменение данных путем пересоздания объектов / массивов
● TypeScript не поддерживает иммутабельные типы данных в runtime
○ Но скоро могут появятся записи (records) и кортежи (tuples)
● Ключевое слово readonly
○ ReadonlyArray
○ ReadonlyRecord
○ ReadonlyMap
○ ReadonlySet
● eslint-plugin-functional
18. Иммутабельные данные
const arr: readonly number[] = [1, 2, 3];
arr.push(4); // Property 'push' does not exist on type 'readonly number[]'
arr.pop(); // Property 'pop' does not exist on type 'readonly number[]'.
arr.unshift(); // Property 'unshift' does not exist on type 'readonly number[]'.
arr.shift(); // Property 'shift' does not exist on type 'readonly number[]'
arr.splice(1); // Property 'splice' does not exist on type 'readonly number[]'
19. Чистые функции
const sum = (a: number, b: number): number => a + b;
const multiply = (a: number, b: number): number => a * b;
const subtract = (a: number, b: number): number => a - b;
const divide = (a: number, b: number): number => a / b;
const dateToString = (date: Date): string => date.toDateString();
20. Чистые каррированые функции
const sum = (a: number) => (b: number): number => a + b;
const multiply = (a: number) => (b: number): number => a * b;
const byEmail = (email: string) => (user: User): boolean =>
user.email === email;
// Удобно применять когда функция ожидает предикат на вход
const user = users.find(byEmail("peter-parker@gmail.com"));
21. Чистые функции могут мутировать данные внутри себя
function shuffle<T>(array: readonly T[]): readonly T[] {
const arrayCopy = [...array];
for (let i = arrayCopy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = arrayCopy[i];
arrayCopy[i] = array[j];
arrayCopy[j] = temp;
}
return arrayCopy;
}
22. Побочные эффекты
Побочный эффект - это изменение состояния системы или наблюдаемое
взаимодействие с окружающим миром, происходящее во время вычисления
результата.
● изменения в файловой системе
● обращение к базе данных
● выполнение http-запроса
● выброс ошибок
● вывод на экран / запись в лог
● получение данных от пользователя
● выполнение запроса к DOM
Любое взаимодействие со средой вне функции является побочным эффектом.
24. “Честные” сигнатуры функций
type User = {
readonly id: string;
readonly email: string;
};
declare function createUser(email: string): User;
25. Скрытые побочные эффекты внутри функции
function createUser(email: string): User {
if (email.length > 256) {
throw new Error("Email is too long!");
}
if (!email.includes("@")) {
throw new Error("Email is invalid!");
}
return { id: uuid(), email };
}
29. Композиция функций с применением pipe
import {pipe} from "fp-ts/function";
const value = 2;
const result = pipe(value, sum(2), multiply(3),
valueToString);
console.log(result); // 12
34. declare function getUsers(): readonly User[];
declare function createFullName(user: User): string;
const users = getUsers();
const result = pipe(
users.find(byEmail("spider-man@gmail.com")),
createFullName
);
Проблема композиции функций. Код скомпилируется?
35. Проблема композиции функций
// Argument of type 'User | undefined' is not
assignable to parameter of type 'User'.
// Type 'undefined' is not assignable to type 'User'.
const result = pipe(
users.find(byEmail("spider-man@gmail.com")),
createFullName
);
38. Объединения типов с дискриминантом
Объединения типов с дискриминантом - это объединения у которых
присутствует одно общее поле (с уникальным символом, чаще всего
строковым литералом).
Оно и будет служить дискриминантом, чтобы TypeScript по нему вывел что
же за тип у вас сейчас в руках.
39. Объединения типов с дискриминантом
type Success<T> = {
readonly type: "success";
readonly value: T;
}
type Fail = {
readonly type: "fail";
readonly error: Error;
}
type Result<T> = Success<T> | Fail;
40. Объединения типов с дискриминантом
declare function loadUsers(): Result<readonly User[]>;
const result = loadUsers();
// Property 'value' does not exist on type 'Result<readonly User[]>'
console.log(result.value);
// Property 'error' does not exist on type 'Result<readonly User[]>'
console.log(result.error);
41. Объединения типов с дискриминантом
switch (result.type) {
case "success":
console.log(result.value);
break;
case "fail":
console.log(result.error);
break;
default:
absurd(result); // typeof result is never
}
42. Тело функции absurd
function absurd<A>(_: never): A {
throw new Error('Called `absurd` function which
should be uncallable')
}
44. Option
Option - конструкция, описывающая наличие или отсутствие значения.
С помощью функций map и chain позволяет работать с цепочками вызовов
функций даже если какая-то из них может вернуть null / undefined.
Option - тип данных, предоставляемый библиотекой fp-ts
● Описание типа
● Функции для взаимодействия
45. Описание типа Option
type None = {
readonly _tag: 'None';
};
type Some<A> = {
readonly _tag: 'Some';
readonly value: A;
};
type Option<A> = None | Some<A>;
52. Пример использование Option
import {findFirst} from "fp-ts/ReadonlyArray";
// Option<User>
// { _tag: 'Some', value: { id: 1, email: "spiderman@gmail.com" } }
const value = pipe(
users,
findFirst(byEmail("spider-man@gmail.com"))
);
53. Пример использование Option
import { map } from "fp-ts/Option";
// Option<string>
// { _tag: 'Some', value: 'Peter Parker' }
const value = pipe(
users,
findFirst(byEmail("spider-man@gmail.com")),
map(createFullName)
);
54. Пример использование Option
import { map } from "fp-ts/Option";
// Option<string>
// { _tag: 'None' }
const value = pipe(
users,
findFirst(byEmail("mary-jane-watson@gmail.com")),
map(createFullName)
);
55. Описание типа функции Map для Option
type OptionMap =
<A, B>(f: (a: A) => B) =>
(fa: Option<A>) => Option<B>;
56. Реализация функции Map для Option
const map: OptionMap = f => option => {
switch (option._tag) {
case "None":
return option;
case "Some":
return { _tag: "Some", value: f(option.value) };
default:
return absurd(option)
}
}
57. map-ов может быть много
declare function stringHash(value: string): number;
// Option<number>
const value = pipe(
users,
findFirst(byEmail("spider-man@gmail.com")),
map(createFullName),
map(stringHash)
);
58. Функтор — это класс типов, для которых определена функция map и
соблюдаются некоторые законы:
● Закон идентичности
● Закон композиции
Законы - это ограничения, которые накладываются на реализацию функции
map.
В ежедневной работе Вам не придется писать свои функторы. Все
необходимые функторы реализованы в библиотеке fp-ts.
Функтор
61. Как избавиться от вложенных Option<T>
import {chain} from "fp-ts/ReadonlyArray";
const comments: Record<UserId, readonly Comment[]> = {};
// Option<readonly Comment[]>
const value = pipe(
users,
findFirst(byEmail("spider-man@gmail.com")),
chain(user => lookup(user.id, comments))
)
62. Описание типа функции Chain для Option
type OptionChain =
<A, B>(f: (a: A) => Option<B>) =>
(fa: Option<A>) => Option<B>;
63. Реализация функции Chain для Option
const chain: OptionChain = f => option => {
switch (option._tag) {
case "None":
return option;
case "Some":
return f(option.value);
default:
return absurd(option)
}
}
64. Монада
Монада - это класс типов, для которой определены функции map, chain, of и
соблюдаются некоторые законы:
● Закон идентичности
● Закон ассоциативности
Законы - это ограничения, которые накладываются на реализацию функции
chain.
В ежедневной работе Вам не придется писать свои монады. Все
необходимые монады реализованы в библиотеке fp-ts.
Монады используются для последовательных вычислений.
66. Как достать значение из Option?
import {toUndefined} from "fp-ts/Option";
// string | undefined
const value = pipe(
users,
findFirst(byEmail("spider-man@gmail.com")),
map(createFullName),
toUndefined
);
67. Как достать значение из Option?
import {constant} from "fp-ts/function";
import {getOrElse} from "fp-ts/Option";
// string
const value = pipe(
users,
findFirst(byEmail("spider-man@gmail.com")),
map(createFullName),
getOrElse(constant<string>("NO_NAME"))
);
68. Что еще интересного есть в fp-ts?
● Either - для работы с ошибками
● IO - для работы с побочными эффектами
● Task - для работы с асинхронным кодом
● TaskEither - для работы с асинхронным кодом, который может вернуть ошибку
● Reader - внедрение зависимостей в функциональном стиле
● State - для хранения состояния в композиции функций
● ReaderTaskEither - внедрение зависимостей в асинхронную функцию, которая
может вернуть ошибку
● Eq - для сравнения данных по значению
● Ord - для сортировки данных
● Semigroup - для конкатенации данных
● Monoid - для “сворачивания” данных (reduce)
69. Что почитать? Код из презентации
https://github.com/dkhovrich/fp-ts-talk-code#readme
71. Вакансия в команду Spark
● продуктовая компания и возможность влиять на сам продукт
● команда профессионалов
● стек технологий (TypeScript, React, RxJS, fp-ts)
● пробуем новые инструменты, подходы
● достойный компенсационный пакет от компании
● гибридный гибкий график (офис, ремоут)
Все вакансии Readdle - https://readdle.com/careers 🚀