[Программирование, Функциональное программирование, TypeScript] Функциональное программирование на TypeScript: паттерн «класс типов»
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Предыдущие статьи цикла:
В предыдущей статье я рассказал, как можно в TypeScript эмулировать полиморфизм родов высшего порядка. Давайте же теперь посмотрим, какие возможности это даёт функциональному программисту, и начнем мы с паттерна «класс типов» (type class).
Само понятие класса типов пришло из Haskell и было предложено впервые Филипом Уодлером и Стивеном Блоттом в 1988 году для реализации ad hoc-полиморфизма. Класс типов определяет множество типизированных функций и констант, которые должны существовать для каждого типа, который принадлежит данному классу. Поначалу звучит сложно, но на самом деле это достаточно простая и элегантная конструкция.
Что такое класс типов
Сразу оговорка для тех, кто хорошо разбирается в Haskell или Scala
SPL
В этой статье я буду давать упрощенное объяснение концепции классов типов, не затрагивающее словарь инстансов, разрешение конфликта инстансов и механизм вывода типов. Всё-таки TypeScript и JavaScript как его рантайм обладают существенно более простой системой типов, в которой отсутствует механизм неявной передачи аргументов в функцию (кроме this). Поэтому то, что будет описано ниже, скорее будет походить на GHC Core Language, где классы типов передаются как явные аргументы.
Рассмотрим в качестве примера один из простейших классов типов — Show, — который определяет операцию приведения к строке. Он определен в модуле fp-ts/lib/Show:
interface Show<A> {
readonly show: (a: A) => string;
}
Это определение читается так: тип A принадлежит классу Show, если для A определена функция show : (a: A) => string.
Реализуется класс типов следующим образом:
const showString: Show<string> = {
show: s => JSON.stringify(s)
};
const showNumber: Show<number> = {
show: n => n.toString()
};
// Предположим, что есть тип «пользователь» с полями name и age:
const showUser: Show<User> = {
show: user => `User "${user.name}", ${user.age} years old`
};
Вся сила классов типов проявляется в их композиции. К примеру, мы легко можем написать реализацию класса типов Show для определенной структуры — скажем, кортежа, — если у нас будет экземпляр Show для содержимого этой структуры:
// В данном случае использование any оправданно, т.к. для типа T не важна
// конкретная типизация Show — она будет уточнена далее с помощью infer.
// Такой трюк позволяет исключить из T все элементы, которые не являются
// экземплярами Show:
const getShowTuple = <T extends Array<Show<any>>>(
...shows: T
): Show<{ [K in keyof T]: T[K] extends Show<infer A> ? A : never }> => ({
show: t => `[${t.map((a, i) => shows[i].show(a)).join(', ')}]`
});
Использование классов типов позволяет использовать подход наименьшего знания (principle of least knowledge, principle of least power) — когда функция запрашивает от своих аргументов только тот набор функциональных возможностей, который будет ей использован. В TypeScript за счет структурной типизации этот подход воспринимается очень органично, и использование классов типов позволяет развить эту идею.
Давайте рассмотрим еще один синтетический пример — нам надо написать функцию, которая для произвольной структуры данных приводит ее содержимое к строкам. Благодаря трюку из предыдущей статьи, классы типов можно писать не только для конкретных типов, но и для типов высшего порядка. Тип Mappable, он же Functor — это как раз пример такого класса типов. Функтор позволяет выполнять преобразования с сохранением структуры — к примеру, если у нас есть список, то операция map изменит тип элементов, но сохранит порядок в этом списке; если у нас есть дерево — то map сохранит последовательность ветвей и узлов; если у нас есть хэш-таблица — map сохранит ключи нетронутыми. Функтор как раз и позволит нам решить поставленную задачу:
import { Kind } from 'fp-ts/lib/HKT';
import { Functor } from 'fp-ts/lib/Functor';
import { Show } from 'fp-ts/lib/Show';
const stringify = <F extends URIS, A>(F: Functor<F>, A: Show<A>) =>
(structure: Kind<F, A>): Kind<F, string> => F.map(structure, A.show);
Казалось бы, много «синтаксического шума» и непонятные преимущества, так? Но не спешите скептически вздымать бровь — давайте посмотрим, насколько большую гибкость дает использование такого подхода.
Отделение интерфейса класса типов от конкретной реализации позволяет писать полиморфный код, который будет сохранять работоспособность даже в случае изменения структуры данных. Предположим, вы пишете модуль комментариев для своего блога, и в первой реализации решаете, что ваши нужды удовлетворит простая линейная структура — поэтому решаете хранить комментарии в обычном списке:
interface Comment {
readonly author: string;
readonly text: string;
readonly createdAt: Date;
}
const comments: Comment[] = ...;
const renderComments = (comments: Comment[]): Component => <List>{comments.map(renderOneComment)}</List>;
const renderOneComment = (comment: Comment): Component => <ListItem>{comment.text} by {comment.author} at {comment.createdAt}</ListItem>
Когда вы поймете, что хорошо бы комментарии хранить в дереве, а не списке, вам придется переписать все места, где с коллекцией comments обращаются как со списком.
Но вы можете воспользоваться подходом с классами типов, и организовать код несколько иначе:
interface ToComponent<A> {
readonly render: (element: A) => Component;
}
const commentToComponent: ToComponent<Comment> = {
render: comment => <>{comment.text} by {comment.author} at {comment.createdAt}</>
};
const arrayToComponent = <A>(TCA: ToComponent<A>): ToComponent<Comment[]> => ({
render: as => <List>{as.map(a => <ListItem>{TCA.render(a)}</ListItem>)}</List>
});
const treeToComponent = <A>(TCA: ToComponent<A>): ToComponent<Tree<Comment>> => ({
render: treeA => <div class="node">
{TCA.render(treeA.value)}
<div class="inset-relative-to-parent">
{treeA.children.map(treeToComponent(TCA).render)}
</div>
</div>
});
const renderComments =
<F extends URIS>(TCF: ToComponent<Kind<F, Comment>>) =>
(comments: Kind<F, Comment>) => TCF.render(comments);
...
// где-то в родительском компоненте вы просто заменяете это:
const commentArray: Comment[] = getFlatComments();
renderComments(arrayToComponent(commentToComponent))(commentArray);
// ...на это, не трогая остальной код рендера:
const commentTree: Tree<Comment> = getCommentHierarchy();
renderComments(treeToComponent(commentToComponent))(commentTree);
В целом, использование классов типов как паттерна проектирования в TypeScript можно описать так:
- Функциональность, которая может быть обобщена, выносится из базового типа данных в отдельный интерфейс, полиморфный по типу данных или типу контейнера.
- Каждая функция, которая хочет использовать эту функциональность, «запрашивает» нужный набор классов типов как первый каррированный аргумент. Это делается для того, чтобы не завязываться на конкретный экземпляр/instance класса типов — в результате получается более гибкое и тестируемое решение.
- Для различения экземплятор классов типов от обычных аргументов функции есть смысл давать им имена в UPPER_SNAKE_CASE, чтобы их использование бросалось в глаза на фоне camelCase в остальном коде. Понятно, что это хорошо работает в случае, если вы пишете идиоматично — если же ваш код $tyled_like_php, то вам стоит придумать свою нотацию.
Некоторые полезные классы типов
В библиотеке fp-ts представлено достаточно много классов типов, в которых есть смысл разбираться, если вы хотите понимать подходы «взрослого» ФП.
Functor (fp-ts/lib/Functor)
Функтор определен операцией map : <A, B>(f: (a: A) => B) => (fa: F<A>) => F<B>, которую можно рассматривать с двух точек зрения:
- Функтор для какого-либо вычислительного контекста F знает, как применить чистую функцию A => B к значению F<A>, чтобы получилось F<B>.
- Функтор умеет поднять чистую функцию A => B в вычислительный контекст F так, что получается функция F<A> => F<B>.
Оба эти определения равноценны, но первое, по моему опыту, проще воспринимается разработчиками, а второе ближе математикам-теоркатегорщикам. В любом случае, суть одна — функтор позволяет изменить данные внутри какого-либо контекста без изменения структуры этого контекста.
Любой экземпляр функтора должен подчиняться двум законам:
- Сохранение идентичности: map(id) ≡ id
- Сохранение композиции функций: map(compose(f, g)) ≡ compose(map(f), map(g))
С функторами мы уже сталкивались в предыдущей и этой статье, и их полезность в целом нельзя преуменьшить — функторы дают начало целой плеяде других классов типов, поэтому если вы знаете, что у вас есть, к примеру, экземпляр монады, то автоматически у вас есть операция из класса типов Functor map.
Monad (fp-ts/lib/Monad)
О, эта ужасная монада, она же буррито, она же railway, она же моноид в моноидальной категории эндофункторов. На самом деле, монада это предельно простая штука. Внимание, сейчас будет самый короткий монадический туториал!
Монада определяется правилом «1-2-3»: 1 тип, 2 операции и 3 закона:
- Монада может быть определена для типа высшего порядка — скажем, для конструкторов типов вроде Array, List, Tree, Option, Reader и т.д. — словом, всего, что мы привыкли видеть в дженериковой форме.
- Монада может определена двумя операциями, причем одним из двух равнозначных путей — операции chain и join выражаются друг через друга, поэтому для описания монады достаточно только of и одной из этих двух операций:
- Первый способ:
of : <A>(value: A) => F<A>
chain : <A, B>(f: (a: A) => F<B>) => (fa: F<A>) => F<B>
- Второй способ:
of : <A>(value: A) => F<A>
join : <A>(ffa: F<F<A>>) => F<A>
- Наконец, любая монада должна подчиняться трём законам:
- Закон идентичности слева: chain(f)(of(a)) ≡ f(a)
- Закон идентичности справа: chain(of)(m) ≡ m
- Закон ассоциативности: chain(g)(chain(f)(m)) ≡ chain(x => chain(g)(f(x)))(m)
С синтаксисом хаскеля эти законы воспринимаются куда проще
SPL
В хаскеле of это pure, а chain это инфиксный оператор >>= (читается «bind»):
- Закон идентичности слева: pure a >>= f ≡ f a
- Закон идентичности справа: m >>= pure ≡ m
- Закон ассоциативности: (m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)
Всё, туториал окончен, всем спасибо, все свободны. Домашнее задание: написать экземпляр монады для типа type Reader<R, A> = (env: R) => A.
Зная это определение, вы можете сказать, что знаете, что такое монада. В них нет ничего мистического, ничего неявного и ничего сакрального — это просто тип, две операции и три закона, точка. С законами в языках без зависимой типизации дела обстоят несколько сложно, поэтому их есть смысл проверять с помощью тестирования через свойства (property-based testing).
Монада выражает идею последовательных вычислений. Посмотрите внимательно на сигнатуру функции chain: один из ее аргументов это «упакованное» в вычислительный контекст F значение типа A, а другое — функция, которая принимает чистое значение типа A, и которая возвращает новый вычислительный контекст со значением типа B. И нет никакого другого способа получить значение типа A из аргумента типа F<A>, кроме как обработать этот вычислительный контекст F. Простейший пример такого поведения — если у нас есть Promise<A>, то получить оттуда значение типа A можно только «подождав» выполнение промиса. К сожалению, сам промис как таковой не соответствует интерфейсу и поведению монады, но концепцию последовательности вычислений им проиллюстрировать можно.
Для удобной работы с монадическими цепочками в нормальных ФП-языках есть синтакцический сахар — do-нотация, for comprehension, — у нас же в TS нет ничего такого. Есть попытки сделать что-то на генераторах, но наиболее типобезопасным вариантом является Do из fp-ts-contrib. В следующих статьях я постараюсь показать его использование.
Monoid (fp-ts/lib/Monoid)
Моноид состоит из:
- Нейтрального элемента, еще называемого единица/unit: empty : A
- Бинарной ассоциативной операции: combine : (left: A, right: A) => A
Моноид также должен подчиняться 3 законам:
- Закон идентичности слева: combine(empty, x) ≡ x
- Закон идентичности справа: combine(x, empty) ≡ x
- Закон ассоциативности: combine(combine(x, y), z) ≡ combine(x, combine(y, z))
Для чего может быть полезен моноид? В первую очередь — там, где мы хотим объединять сущности между собой, и таких мест может быть просто огромное количество. Я не стану здесь расписывать всё, а взамен предложу посмотреть прекрасный доклад Луки Якобовица «Monoids, monoids, monoids». Доклад на английском и для Scala, но суть любой инженер должен уловить достаточно легко — Лука не первый раз читает этот доклад и хорошо доносит мысль.
Существует еще масса полезных классов типов — например, Foldable/Traversable позволяют обходить структуры данных, применяя на каждом шаге определенную операцию в каком-то контексте; Applicative (который я не стал разбирать в этой статье, но обязательно вернусь в статье про типобезопасную валидацию) позволяет применять функцию в контексте к данным в контексте; Task/TaskEither/Future позволяют заменить хаотичные промисы на законопослушные примитивы синхронизации, и так далее. Но я не могу себе позволить раздувать эту статью еще больше. Поэтому на этом я предлагаю данную статью закончить, а в следующей поговорить о более конкретных и практически применимых классах типов и подойти к идее алгебраических эффектов.
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование, Git, Лайфхаки для гиков] Git, я хочу все отменить! Команды исправления допущенных ошибок (перевод)
- [Хостинг, Программирование, DevOps, Микросервисы] Технология Serverless: снова привет, 1970-е (перевод)
- [Информационная безопасность, Системное администрирование, Сетевые технологии] 4. Фишинг в 2020. Пример атаки и обзор решений в мире
- [Open source, Программирование, C++] Интерпретатор скрипта на С++
- [Разработка игр, Читальный зал, Дизайн игр, Игры и игровые приставки] История Burnout (перевод)
- [Программирование] Асинхронное взаимодействие. Брокеры сообщений. Apache Kafka
- [Разработка веб-сайтов, PHP, Drupal, Программирование] #lazy_builder (не путать с lazy load) в Drupal 8/9
- [PHP, Программирование] События в OpenCart
- [Программирование, C++, Проектирование и рефакторинг] Как легко и просто модернизировать код на C++ (перевод)
- [Производство и разработка электроники, Процессоры, IT-компании] Apple разместила первый заказ на чипы с 3-нм техпроцессом у TSMC
Теги для поиска: #_programmirovanie (Программирование), #_funktsionalnoe_programmirovanie (Функциональное программирование), #_typescript, #_typescript, #_ts, #_fp, #_fpts, #_funktsionalnoe_programmirovanie (функциональное программирование), #_programmirovanie (
Программирование
), #_funktsionalnoe_programmirovanie (
Функциональное программирование
), #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:03
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Предыдущие статьи цикла: В предыдущей статье я рассказал, как можно в TypeScript эмулировать полиморфизм родов высшего порядка. Давайте же теперь посмотрим, какие возможности это даёт функциональному программисту, и начнем мы с паттерна «класс типов» (type class). Само понятие класса типов пришло из Haskell и было предложено впервые Филипом Уодлером и Стивеном Блоттом в 1988 году для реализации ad hoc-полиморфизма. Класс типов определяет множество типизированных функций и констант, которые должны существовать для каждого типа, который принадлежит данному классу. Поначалу звучит сложно, но на самом деле это достаточно простая и элегантная конструкция. Что такое класс типов Сразу оговорка для тех, кто хорошо разбирается в Haskell или ScalaSPLВ этой статье я буду давать упрощенное объяснение концепции классов типов, не затрагивающее словарь инстансов, разрешение конфликта инстансов и механизм вывода типов. Всё-таки TypeScript и JavaScript как его рантайм обладают существенно более простой системой типов, в которой отсутствует механизм неявной передачи аргументов в функцию (кроме this). Поэтому то, что будет описано ниже, скорее будет походить на GHC Core Language, где классы типов передаются как явные аргументы.
Рассмотрим в качестве примера один из простейших классов типов — Show, — который определяет операцию приведения к строке. Он определен в модуле fp-ts/lib/Show: interface Show<A> {
readonly show: (a: A) => string; } Это определение читается так: тип A принадлежит классу Show, если для A определена функция show : (a: A) => string. Реализуется класс типов следующим образом: const showString: Show<string> = {
show: s => JSON.stringify(s) }; const showNumber: Show<number> = { show: n => n.toString() }; // Предположим, что есть тип «пользователь» с полями name и age: const showUser: Show<User> = { show: user => `User "${user.name}", ${user.age} years old` }; Вся сила классов типов проявляется в их композиции. К примеру, мы легко можем написать реализацию класса типов Show для определенной структуры — скажем, кортежа, — если у нас будет экземпляр Show для содержимого этой структуры: // В данном случае использование any оправданно, т.к. для типа T не важна
// конкретная типизация Show — она будет уточнена далее с помощью infer. // Такой трюк позволяет исключить из T все элементы, которые не являются // экземплярами Show: const getShowTuple = <T extends Array<Show<any>>>( ...shows: T ): Show<{ [K in keyof T]: T[K] extends Show<infer A> ? A : never }> => ({ show: t => `[${t.map((a, i) => shows[i].show(a)).join(', ')}]` }); Использование классов типов позволяет использовать подход наименьшего знания (principle of least knowledge, principle of least power) — когда функция запрашивает от своих аргументов только тот набор функциональных возможностей, который будет ей использован. В TypeScript за счет структурной типизации этот подход воспринимается очень органично, и использование классов типов позволяет развить эту идею. Давайте рассмотрим еще один синтетический пример — нам надо написать функцию, которая для произвольной структуры данных приводит ее содержимое к строкам. Благодаря трюку из предыдущей статьи, классы типов можно писать не только для конкретных типов, но и для типов высшего порядка. Тип Mappable, он же Functor — это как раз пример такого класса типов. Функтор позволяет выполнять преобразования с сохранением структуры — к примеру, если у нас есть список, то операция map изменит тип элементов, но сохранит порядок в этом списке; если у нас есть дерево — то map сохранит последовательность ветвей и узлов; если у нас есть хэш-таблица — map сохранит ключи нетронутыми. Функтор как раз и позволит нам решить поставленную задачу: import { Kind } from 'fp-ts/lib/HKT';
import { Functor } from 'fp-ts/lib/Functor'; import { Show } from 'fp-ts/lib/Show'; const stringify = <F extends URIS, A>(F: Functor<F>, A: Show<A>) => (structure: Kind<F, A>): Kind<F, string> => F.map(structure, A.show); Казалось бы, много «синтаксического шума» и непонятные преимущества, так? Но не спешите скептически вздымать бровь — давайте посмотрим, насколько большую гибкость дает использование такого подхода. Отделение интерфейса класса типов от конкретной реализации позволяет писать полиморфный код, который будет сохранять работоспособность даже в случае изменения структуры данных. Предположим, вы пишете модуль комментариев для своего блога, и в первой реализации решаете, что ваши нужды удовлетворит простая линейная структура — поэтому решаете хранить комментарии в обычном списке: interface Comment {
readonly author: string; readonly text: string; readonly createdAt: Date; } const comments: Comment[] = ...; const renderComments = (comments: Comment[]): Component => <List>{comments.map(renderOneComment)}</List>; const renderOneComment = (comment: Comment): Component => <ListItem>{comment.text} by {comment.author} at {comment.createdAt}</ListItem> Когда вы поймете, что хорошо бы комментарии хранить в дереве, а не списке, вам придется переписать все места, где с коллекцией comments обращаются как со списком. Но вы можете воспользоваться подходом с классами типов, и организовать код несколько иначе: interface ToComponent<A> {
readonly render: (element: A) => Component; } const commentToComponent: ToComponent<Comment> = { render: comment => <>{comment.text} by {comment.author} at {comment.createdAt}</> }; const arrayToComponent = <A>(TCA: ToComponent<A>): ToComponent<Comment[]> => ({ render: as => <List>{as.map(a => <ListItem>{TCA.render(a)}</ListItem>)}</List> }); const treeToComponent = <A>(TCA: ToComponent<A>): ToComponent<Tree<Comment>> => ({ render: treeA => <div class="node"> {TCA.render(treeA.value)} <div class="inset-relative-to-parent"> {treeA.children.map(treeToComponent(TCA).render)} </div> </div> }); const renderComments = <F extends URIS>(TCF: ToComponent<Kind<F, Comment>>) => (comments: Kind<F, Comment>) => TCF.render(comments); ... // где-то в родительском компоненте вы просто заменяете это: const commentArray: Comment[] = getFlatComments(); renderComments(arrayToComponent(commentToComponent))(commentArray); // ...на это, не трогая остальной код рендера: const commentTree: Tree<Comment> = getCommentHierarchy(); renderComments(treeToComponent(commentToComponent))(commentTree); В целом, использование классов типов как паттерна проектирования в TypeScript можно описать так:
Некоторые полезные классы типов В библиотеке fp-ts представлено достаточно много классов типов, в которых есть смысл разбираться, если вы хотите понимать подходы «взрослого» ФП. Functor (fp-ts/lib/Functor) Функтор определен операцией map : <A, B>(f: (a: A) => B) => (fa: F<A>) => F<B>, которую можно рассматривать с двух точек зрения:
Оба эти определения равноценны, но первое, по моему опыту, проще воспринимается разработчиками, а второе ближе математикам-теоркатегорщикам. В любом случае, суть одна — функтор позволяет изменить данные внутри какого-либо контекста без изменения структуры этого контекста. Любой экземпляр функтора должен подчиняться двум законам:
С функторами мы уже сталкивались в предыдущей и этой статье, и их полезность в целом нельзя преуменьшить — функторы дают начало целой плеяде других классов типов, поэтому если вы знаете, что у вас есть, к примеру, экземпляр монады, то автоматически у вас есть операция из класса типов Functor map. Monad (fp-ts/lib/Monad) О, эта ужасная монада, она же буррито, она же railway, она же моноид в моноидальной категории эндофункторов. На самом деле, монада это предельно простая штука. Внимание, сейчас будет самый короткий монадический туториал! Монада определяется правилом «1-2-3»: 1 тип, 2 операции и 3 закона:
С синтаксисом хаскеля эти законы воспринимаются куда прощеSPLВ хаскеле of это pure, а chain это инфиксный оператор >>= (читается «bind»):
Всё, туториал окончен, всем спасибо, все свободны. Домашнее задание: написать экземпляр монады для типа type Reader<R, A> = (env: R) => A. Зная это определение, вы можете сказать, что знаете, что такое монада. В них нет ничего мистического, ничего неявного и ничего сакрального — это просто тип, две операции и три закона, точка. С законами в языках без зависимой типизации дела обстоят несколько сложно, поэтому их есть смысл проверять с помощью тестирования через свойства (property-based testing). Монада выражает идею последовательных вычислений. Посмотрите внимательно на сигнатуру функции chain: один из ее аргументов это «упакованное» в вычислительный контекст F значение типа A, а другое — функция, которая принимает чистое значение типа A, и которая возвращает новый вычислительный контекст со значением типа B. И нет никакого другого способа получить значение типа A из аргумента типа F<A>, кроме как обработать этот вычислительный контекст F. Простейший пример такого поведения — если у нас есть Promise<A>, то получить оттуда значение типа A можно только «подождав» выполнение промиса. К сожалению, сам промис как таковой не соответствует интерфейсу и поведению монады, но концепцию последовательности вычислений им проиллюстрировать можно. Для удобной работы с монадическими цепочками в нормальных ФП-языках есть синтакцический сахар — do-нотация, for comprehension, — у нас же в TS нет ничего такого. Есть попытки сделать что-то на генераторах, но наиболее типобезопасным вариантом является Do из fp-ts-contrib. В следующих статьях я постараюсь показать его использование. Monoid (fp-ts/lib/Monoid) Моноид состоит из:
Моноид также должен подчиняться 3 законам:
Для чего может быть полезен моноид? В первую очередь — там, где мы хотим объединять сущности между собой, и таких мест может быть просто огромное количество. Я не стану здесь расписывать всё, а взамен предложу посмотреть прекрасный доклад Луки Якобовица «Monoids, monoids, monoids». Доклад на английском и для Scala, но суть любой инженер должен уловить достаточно легко — Лука не первый раз читает этот доклад и хорошо доносит мысль. Существует еще масса полезных классов типов — например, Foldable/Traversable позволяют обходить структуры данных, применяя на каждом шаге определенную операцию в каком-то контексте; Applicative (который я не стал разбирать в этой статье, но обязательно вернусь в статье про типобезопасную валидацию) позволяет применять функцию в контексте к данным в контексте; Task/TaskEither/Future позволяют заменить хаотичные промисы на законопослушные примитивы синхронизации, и так далее. Но я не могу себе позволить раздувать эту статью еще больше. Поэтому на этом я предлагаю данную статью закончить, а в следующей поговорить о более конкретных и практически применимых классах типов и подойти к идее алгебраических эффектов. =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_funktsionalnoe_programmirovanie ( Функциональное программирование ), #_typescript |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:03
Часовой пояс: UTC + 5