Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Статья про Task, TaskEither, ReaderTaskEither

Примеры кода для статьи на Хабре о функциональной замене промисам

Статья: https://habr.com/ru/post/548622

// Task — ленивый примитив асинхронных вычислений
type Task<A> = () => Promise<A>;
// Уникальный идентификатор ресурса — тэг типа (type tag)
const URI = 'Task';
type URI = typeof URI;
// Определение Task как типа высшего порядка (higher-kinded type)
declare module 'fp-ts/HKT' {
interface URItoKind<A> {
[URI]: Task<A>;
}
}
const Functor: Functor1<URI> = {
URI,
map: <A, B>(taskA: Task<A>, transform: (a: A) => B): Task<B> => async () => {
const prevResult = await taskA();
return transform(prevResult);
},
};
const Apply: Apply1<URI> = {
...Functor,
ap: <A, B>(taskA2B: Task<(a: A) => B>, taskA: Task<A>): Task<B> => async () => {
const transformer = await taskA2B();
const prevResult = await taskA();
return transformer(prevResult);
},
};
const ApplyPar: Apply1<URI> = {
...Functor,
ap: <A, B>(taskA2B: Task<(a: A) => B>, taskA: Task<A>): Task<B> => async () => {
const [transformer, prevResult] = await Promise.all([taskA2B(), taskA()]);
return transformer(prevResult);
},
};
const Applicative: Applicative1<URI> = {
...Apply,
of: <A>(a: A): Task<A> => async () => a,
};
const Monad: Monad1<URI> = {
...Applicative,
chain: <A, B>(taskA: Task<A>, next: (a: A) => Task<B>): Task<B> => async () => {
const prevResult = await taskA();
const nextTask = next(prevResult);
return nextTask();
},
};
import * as fs from 'fs';
import { pipe } from 'fp-ts/function';
import * as Console from 'fp-ts/Console';
import * as TE from 'fp-ts/TaskEither';
// Сначала я оберну функции из системного модуля `fs` при помощи `taskify`, сделав их чистыми:
const readFile = TE.taskify(fs.readFile);
const writeFile = TE.taskify(fs.writeFile);
const program = pipe(
// Входная точка — массив задач по чтению трёх файлов с диска:
[readFile('/tmp/file1'), readFile('/tmp/file2'), readFile('/tmp/file3')],
// Для текущей задачи важен порядок обхода массива, поэтому я использую последовательную, а не параллельную версию
// функции traverseArray – `traverseSeqArray`:
TE.traverseSeqArray(TE.map(buffer => buffer.toString('utf8'))),
// При помощи функции `chain` из интерфейса монады я организую последовательность вычислений:
TE.chain(fileContents => writeFile('/tmp/combined-file', fileContents.join('\n\n'))),
// Наконец, в финале я хочу узнать, завершилась ли программа успешно или ошибочно, и залогировать это.
// Тут мне поможет модуль `fp-ts/Console`, содержащий чистые функции по работе с консолью:
TE.match(
err => TE.fromIO(Console.error(`An error happened: ${err.message}`)),
() => TE.fromIO(Console.log('Successfully written to the combined file')),
)
);
// Наконец, запускаем нашу чистую программу на выполнение, выполняя все побочные эффекты:
await program();
// Reader это функция из некоторого окружения типа `E` в значение типа `A`:
type Reader<E, A> = (env: E) => A;
// Reader является типом высшего порядка, поэтому определим всё необходимое:
const URI = 'Reader';
type URI = typeof URI;
declare module 'fp-ts/HKT' {
interface URItoKind2<E, A> {
readonly Reader: Reader<E, A>;
}
}
// Функтор:
const readerFunctor: Functor2<URI> = {
URI,
map: <R, A, B>(fa: Reader<R, A>, f: (a: A) => B): Reader<R, B> => (env) => f(fa(env))
};
// Apply:
const readerApply: Apply2<URI> = {
...readerFunctor,
ap: <R, A, B>(fab: Reader<R, (a: A) => B>, fa: Reader<R, A>): Reader<R, B> => (env) => {
const fn = fab(env);
const a = fa(env);
return fn(a);
}
};
// Аппликативный функтор:
const readerApplicative: Applicative2<URI> = {
...readerApply,
of: <R, A>(a: A): Reader<R, A> => (_) => a
};
// Монада:
const readerMonad: Monad2<URI> = {
...readerApplicative,
chain: <R, A, B>(fa: Reader<R, A>, afb: (a: A) => Reader<R, B>): Reader<R, B> => (env) => {
const a = fa(env);
const fb = afb(a);
return fb(env);
},
};
import { sequenceS } from 'fp-ts/Apply';
import { pipe } from 'fp-ts/function';
import * as R from 'fp-ts/Reader';
import Reader = R.Reader;
const seq = sequenceS(R.Apply);
interface AppConfig {
readonly host: string;
readonly port: number;
readonly connectionString: string;
}
type Database = 'connected to the db';
type Express = 'express is listening';
type App<A> = Reader<AppConfig, A>;
const expressServer: App<Express> = pipe(
R.ask<AppConfig>(),
R.map(
config => {
console.log(`${config.host}:${config.port}`);
return 'express is listening';
},
),
);
const databaseConnection: App<Database> = pipe(
R.asks<AppConfig, string>(cfg => cfg.connectionString),
R.map(
connectionString => {
console.log(connectionString);
return 'connected to the db';
},
),
);
const application: App<void> = pipe(
seq({
db: databaseConnection,
express: expressServer,
}),
R.map(
({ db, express }) => {
console.log([db, express].join());
console.log('app was initialized');
return;
},
),
);
application({
host: 'localhost',
port: 8080,
connectionString: 'mongo://localhost:271017',
});

Пример программы, использующей ReaderTaskEither, можно найти здесь, а видео с разбором — тут.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment