Commit 3ce695a4 by Vladislav Lagunov

Добавлена старая версия @bitmaster/core

parent a1fd6d36
* [Общее описание](#Общее-описание)
* [Either](#Either)
* [Decoder](#Decoder)
* [Eff](#Eff)
* [Контейнеры](#Контейнеры)
- [Функтор (метод `map`)](#Функтор-(метод-`map`))
- [Аппликативный функтор (фунция `ap`)](#Аппликативный-функтор-(фунция-`ap`))
- [Монада (метод `chain`)](#Монада-(метод-`chain`))
- [Traversable (метод `traverse`)](#Traversable-(метод-`traverse`))
- [Alternative (метод `oneOf`)](#Alternative-(метод-`oneOf`))
---
# Общее описание
Библиотека является адаптацией [elm-архитектуры](https://guide.elm-lang.org/architecture/) для
языка [typescript](http://www.typescriptlang.org/), похожие библиотеки
— (https://github.com/gcanti/fp-ts), (https://github.com/gcanti/elm-ts)
```sh
$ tree examples src
examples # примеры использования библиотеки
src
├── cmd.ts # `Cmd` — контейнер для типа `Action`, используeтся в функции `update`
├── decode.ts # библиотека для валидации JSON
├── eff.ts # `Eff` — контейнер побочных эффектов
├── either.ts # `Either` — контейнер для значений, которые могут содержать ошибку
├── http.ts # обертка над XmlHttpRequest
├── index.ts # импорт наиболее часто используемых функций
├── internal
│   └── expr.ts
├── optics.ts # фунциональные линзы и призмы
├── option.ts # `Option` или `Maybe` — контейнер для значений, которые могут отсутствовать
├── quickcheck
│   ├── arbitrary.ts
│   └── generator.ts
├── quickcheck.ts # библиотека для генеративного тестирования
└── sub.ts
```
# Either
Сумма (или объединение) двух типов `err` и `succ`. `Either` нельзя определить как `type Either<err,succ> = err | succ;`
потомучто нельзя различить является ли тип `err` или `succ` когда
оба типа равны. `Either` часто используется как результат вычислений которые могут закончится
неудачно, `err` — тип результата с ошибкой, `succ` — тип успешного результата,
```js
type Either<err,succ> = Left<err,succ> | Right<err,succ>;
// пример использования
function safeDiv(n: number, d: number): Either<string, number> {
if (d === 0) return failure('Division by zero');
return success(n / d);
}
const fiveByTwo = safeDiv(5, 2) // Right { value: 2.5 }
const oneByZero = safeDiv(1, 0) // Left { value: 'Division by zero' }
```
# Decoder
Позволяет декларативно описывать валидаторы для JSON.
```js
type Validation<a> = Either<Problem, a>;
interface Decoder<a> {
validate(value: any): Either<Problem, a>;
}
// пример использования
import * as t from './core/decode';
console.dir(t.string.validate(undefined)); // Left { value: 'Not a string' }
console.dir(t.string.validate('Hello, sailor!')); // Right { value: 'Hello, sailor!' }
const authDecoder = t.record({
token: t.string,
expires: t.date,
});
console.dir(authDecoder.validate(NaN)); // Left { value: 'not an object' }
console.dir(authDecoder.validate({ token: 'secret', date: '2017-08-23T15:08:31.244Z' })); // Right { value: { token: 'secret', date: '2017-08-23T15:08:31.244Z' } }
```
# Eff
Очень похож на `Promise`, содержит вычисления с побочными эффектами, вместо `then` используется `chain`
```js
export type Eff<err,succ> = Lazy<err,succ> | Async<err,succ>
// пример использования
import * as http from './core/http';
const effect = http.get('http://example.com/api/v1/login')
.chainEff(() => get('http://example.com/api/v1/profile'))
.chainEff(() => get('http://example.com/api/v1/settings));
effect.subscribe(); // выполнение еффекта
```
# Контейнеры
В функциональном программировании вместо мутабельных классов и их инстансов активно используются
неизменяемые типы параметризованные другими типами «контейнеры типов». `Cmd`, `Eff`, `Either`,
`Decoder` являются примерами таких контейнеров. Для них определены несколько общих операций
(комбинаторов), наличие которых определяет принадлежность контейнера к определенному классу (не
путать с ООП классом), подобно тому как реализация определенных методов может означать реализацию
интерфейса в ООП-языках. Пример таких операций: `map`, `ap`, `chain`.
## Функтор (метод `map`)
Наиболее распространенный контейнер типов, функтор должен реализовать фунцию `map` имеющую тип
листинге ниже. Проще говоря функтор должен реализовать возможность применить чистую функцию к
значению хранящемся внутри него.
```js
function map<a,b>(a: F<a>, f: (a: a) => b): F<b>;
// пример использования в typesript
import { Either, success, failure } from './core/either';
const ethr: Either<string, number> = success(8);
const isEven = x => x % 2 === 0;
console.dir(ethr.map(isEven)); // Right { value: true }
```
## Аппликативный функтор (фунция `ap`)
Позволяет применить фунцию с произвольным количеством аргументов к такому же количеству контейнеров
```js
function of<a>(a: a): F<a>;
function ap<a,b,c>(a: F<a>, b: F<b>..., func: (a: a, b: b...) => c)): F<c>;
// пример использования в typescript
import { Either, success, failure } from './core/either';
import * as either from './core/either';
const one: Either<string, number> = success(1);
const two: Either<string, number> = success(2);
const three: Either<string, number> = success(3);
const falty: Either<string, number> = failure('Failed to get a value');
const sum1 = either.ap(one, two, three, (a, b, c) => a + b + c);
const sum2 = either.ap(one, two, falty, (a, b, c) => a + b + c);
console.dir(sum1); // Right { value: 6 }
console.dir(sum2); // Left { value: 'Failed to get a value' }
```
## Монада (метод `chain`)
Позволяет комбинировать вычисления подобно тому как промисы комбинируются через метод `then`
```js
function chain<a,b>(a: M<a>, func: (a: a) => M<b>)): M<b>;
// пример использования в typesript
import { Eff } from './core/eff';
import * as http from './core/http';
const body = { login: 'admin', password: 'admin' };
const authentificate = http.post('api/v1/login', { body })
.chainEff(response => http.get('api/v1/profile'))
.chainEff(response => http.get('api/v1/settings'));
authentificate.subscribe();
```
## Traversable (метод `traverse`)
Некоторые контейнеры реализуют функцию `traverse` с помощью которой,
некоторые действия (определенные контейнером) можно применить к
массиву
```js
function traverse<a,b>(a: a[], func: (a: a) => T<b>)): T<b[]>;
// пример использования в typesript
import { Eff } from './core/eff';
import * as http from './core/http';
const resourceIds = ['10', '11', '12', '99'];
eff.traverse(resourceIds, id => http.get('api/v1/item', id));
authentificate.subscribe();
```
## Alternative (метод `oneOf`)
Некоторые контейнеры реализуют функцию `oneOf` с помощью которой,
можно объединить несколько альтернативных вариантов
```js
function oneOf<a, b>(a: A<a>, b: A<b>): A<a|b>;
// пример использования в typesript
import { gen, Generator } from './core/quickcheck';
type Action = { tag: 'Increment' } | { tag: 'Decrement' } | { tag: 'Set', value: number }
const actionGenerator: Generator<Action> = gen.oneOf(
gen.record({ tag: gen.of('Increment') }),
gen.record({ tag: gen.of('Decrement') }),
gen.record({ tag: gen.of('Set'), value: gen.nat }),
);
console.log(actionGenerator.ntimes(10).generate());
```
import * as React from 'react';
import { isObservable, Observable } from 'rxjs';
// Props
export interface Props<A = any> {
to: EventTarget|Observable<A>;
event?: string;
handler(x: A): void;
}
// Component
class Sub extends React.Component<Props, {}> {
unlisten?: Function|null;
componentDidMount() {
this.listen(this.props);
}
componentWillReceiveProps(props) {
if (this.props.to !== props.to) {
this.unlisten && this.unlisten();
this.listen(props);
}
}
componentWillUnmount() {
this.unlisten && this.unlisten();
}
listen(props: Props) {
const { to, event } = props;
if (isObservable(to)) {
const subscription = to.subscribe(this.handler);
this.unlisten = () => subscription.unsubscribe();
} else {
if (!event) throw new Error('Sub: unspecified `event`');
(to as EventTarget).addEventListener(event, this.handler);
this.unlisten = () => (to as EventTarget).removeEventListener(event, this.handler);
}
}
handler = e => { this.props.handler(e); }
render() { return null as any; }
}
export default Sub;
export * from '../decoder';
import { Either } from '../either';
import { Option } from '../maybe';
import * as either from '../either';
import { Observable } from 'rxjs';
import * as Rx from 'rxjs';
import { map } from 'rxjs/internal/operators/map';
import { combineAll } from 'rxjs/internal/operators/combineAll';
import { Expr } from './internal/expr';
/**
* Тайп-алиасы для удобства
*/
export type Eff<Error, Success> = Observable<Either<Error, Success>>;
export type Cmd<Action> = Eff<never, Action>;
declare module 'rxjs/internal/Observable' {
export interface Observable<T> {
readonly _T: T;
map<L,R,R2>(this: Eff<L, R>, f: (x: R) => R2): Eff<L, R2>;
mapTo<L,R,R2>(this: Eff<L, R>, x: R2): Eff<L, R2>;
mapError<L,R,L2>(this: Eff<L, R>, f: (x: L) => L2): Eff<L2, R>;
mapErrorTo<L,R,L2>(this: Eff<L, R>, x: L2): Eff<L2, R>;
perform<L,R,A>(this: Eff<L, R>, onFailure: (x: L) => A, onSuccess: (x: R) => A): Cmd<A>;
perform<L,R,A1,A2>(this: Eff<L, R>, onFailure: (x: L) => A1, onSuccess: (x: R) => A2): Cmd<A1|A2>;
performMaybe<L,R,A>(this: Eff<L, R>, onFailure: (x: L) => Option<A>, onSuccess: (x: R) => Option<A>): Cmd<A>;
performMaybe<L,R,A1,A2>(this: Eff<L, R>, onFailure: (x: L) => Option<A1>, onSuccess: (x: R) => Option<A2>): Cmd<A1|A2>;
performForget<L, R>(this: Eff<L, R>): Cmd<never>;
performSuccess<R,A>(this: Eff<never, R>, onSuccess: (x: R) => A): Cmd<A>;
onError<L,R,L2,R2>(this: Eff<L, R>, onFailure: (x: L) => Eff<L2, R2>): Eff<L2, R2|R>;
chain<L,R,R2>(this: Eff<L, R>, f: (x: R) => Eff<L, R2>): Eff<L, R2>;
chain<L,R,L2,R2>(this: Eff<L, R>, f: (x: R) => Eff<L2, R2>): Eff<L|L2, R2>;
chainTo<L,R,R2>(this: Eff<L, R>, effect: Eff<L, R2>): Eff<L, R2>;
chainTo<L,R,L2,R2>(this: Eff<L, R>, effect: Eff<L2, R2>): Eff<L|L2, R2>;
// DEPRECATED
chainEff<L,R,R2>(this: Eff<L, R>, f: (x: R) => Eff<L, R2>): Eff<L, R2>;
chainEff<L,R,L2,R2>(this: Eff<L, R>, f: (x: R) => Eff<L2, R2>): Eff<L|L2, R2>;
mapEff<L,R,R2>(this: Eff<L, R>, f: (x: R) => R2): Eff<L, R2>;
mapCmd<L,R,R2>(this: Eff<L, R>, f: (x: R) => R2): Eff<L, R2>;
}
}
(Observable as any).prototype.map = function<L, R, R2>(this: Eff<L, R>, f: (x: R) => R2): Eff<L, R2> {
return this.pipe(map(ethr => ethr.map(f)));
};
(Observable as any).prototype.mapTo = function<L, R, R2>(this: Eff<L, R>, x: R2): Eff<L, R2> {
return this.pipe(map(ethr => ethr.map(() => x)));
};
(Observable as any).prototype.mapError = function<L, R, L2>(this: Eff<L, R>, f: (x: L) => L2): Eff<L2, R> {
return this.pipe(map(ethr => ethr.mapLeft(f)));
};
(Observable as any).prototype.mapErrorTo = function<L, R, L2>(this: Eff<L, R>, x: L2): Eff<L2, R> {
return this.pipe(map(ethr => ethr.mapLeft(() => x)));
};
(Observable as any).prototype.onError = function<L,R,L2,R2>(this: Eff<L, R>, onFailure: (x: L) => Eff<L2, R2>): Eff<L2, R2|R> {
return this.pipe(map((ethr: Either<any, any>) => ethr.tag === 'Left' ? onFailure(ethr.value) : Rx.of(ethr)), combineAll(x => x));
};
(Observable as any).prototype.chain = function<L,R,R2>(this: Eff<L, R>, f: (x: R) => Eff<L, R2>): Eff<L, R2> {
return this.pipe(map(ethr => ethr.tag === 'Right' ? f(ethr.value) : Rx.of(ethr)), combineAll(x => x as Either<any , any>));
};
(Observable as any).prototype.chainTo = function<L,R,R2>(this: Eff<L, R>, x: Eff<L, R2>): Eff<L, R2> {
return this.pipe(map(ethr => ethr.tag === 'Right' ? x : Rx.of(ethr)), combineAll(x => x as Either<any , any>));
};
(Observable as any).prototype.perform = function<L, R, A>(this: Eff<L, R>, onFailure: (x: L) => A, onSuccess: (x: R) => A): Cmd<A> {
return this.pipe(map(ethr => either.success(ethr.fold(onFailure, onSuccess))));
};
(Observable as any).prototype.performMaybe = function<L, R, A>(this: Eff<L, R>, onFailure: (x: L) => Option<A>, onSuccess: (x: R) => Option<A>): Cmd<A> {
return this.pipe(map(projectMaybe), combineAll(x => x as Either<never, A>));
function projectMaybe(ethr: Either<L, R>): Eff<never, A> {
const maybeMessage = ethr.fold(onFailure, onSuccess);
switch (maybeMessage.tag) {
case 'None': return Rx.NEVER;
case 'Some': return Rx.of(either.success(maybeMessage.value));
}
}
};
(Observable as any).prototype.performForget = function<L, R>(this: Eff<L, R>): Cmd<never> {
return this.pipe(map(x => Rx.NEVER), combineAll(x => x));
};
// DEPRECATED
(Observable as any).prototype.chainEff = Observable.prototype.chain;
(Observable as any).prototype.mapEff = Observable.prototype.map;
(Observable as any).prototype.mapCmd = Observable.prototype.map;
export function failure<L extends Expr>(error: L): Eff<L, never> {
return Rx.of(either.failure(error));
}
export function success<R extends Expr>(success: R): Eff<never, R> {
return Rx.of(either.success(success));
}
export function callback(run: () => void): Eff<never, null> {
return Observable.create(observer => (run(), observer.next(either.success(null)), observer.complete(), void 0));
}
export function fromCallback<L, R>(run: (cb: (x: Either<L, R>) => void) => void): Eff<L, R> {
return Observable.create(observer => (run(x => (observer.next(x), observer.complete())), void 0));
}
export function promise<L, R>(func: (...args: Array<any>) => Promise<Either<L, R>>, ...args: Array<any>): Eff<L, R> {
return Observable.create(observer => { func.apply(undefined, args).then(x => (observer.next(x), observer.complete())).catch(e => (observer.error(e), observer.complete())); })
}
export { success as of };
export function lazy<A>(f: () => A): Observable<A>;
export function lazy<A,B>(f: (a: A) => B, a: A): Observable<B>;
export function lazy<A,B,C>(f: (a: A, b: B) => C, a: A, b: B): Observable<C>;
export function lazy<A,B,C,D>(f: (a: A, b: B, c: C) => D, a: A, b: B, c: C): Observable<D>;
export function lazy() {
const _arguments = arguments;
return Observable.create(observer => {
const result = _arguments[0].apply(undefined, Array(_arguments.length - 1).map((_, idx) => _arguments[idx + 1]));
observer.next(result);
observer.complete();
});
}
export function ap<L,A,B>(a: Eff<L,A>, f: (a: A) => B): Eff<L,B>;
export function ap<L,A,B,C>(a: Eff<L,A>, b: Eff<L,B>, f: (a: A, b: B) => C): Eff<L,C>;
export function ap<L,A,B,C,D>(a: Eff<L,A>, b: Eff<L,B>, c: Eff<L,C>, f: (a: A, b: B, c: C) => D): Eff<L,D>;
export function ap<L,A,B,C,D,E>(a: Eff<L,A>, b: Eff<L,B>, c: Eff<L,C>, d: Eff<L,D>, f: (a: A, b: B, c: C, d: D) => E): Eff<L,E>;
export function ap<L,A,B,C,D,E,F>(a: Eff<L,A>, b: Eff<L,B>, c: Eff<L,C>, d: Eff<L,D>, e: Eff<L,E>, f: (a: A, b: B, c: C, d: D, e: E) => F): Eff<L,F>;
export function ap<L,A,B,C,D,E,F,G>(a: Eff<L,A>, b: Eff<L,B>, c: Eff<L,C>, d: Eff<L,D>, e: Eff<L,E>, f_: Eff<L,F>, f: (a: A, b: B, c: C, d: D, e: E, f: F) => G): Eff<L,G>;
export function ap(): Eff<any, any> {
const _arguments = arguments;
return Rx.combineLatest(Array.apply(undefined, Array(arguments.length - 1)).map((_, idx) => _arguments[idx]), (...inputs) => either.traverse(inputs, x => x as any).map(inputs => _arguments[_arguments.length - 1].apply(undefined, inputs)));
}
/** traverse an array */
export function traverse<ERR, A, B>(array: A[], f: (a: A, idx: number) => Eff<ERR, B>): Eff<ERR, B[]> {
if (array.length === 0) return success([]);
// @ts-ignore
return ap(...array.map(f), (...args) => args);
}
/**
* Объединение нескольких параллельно выполняемых `Cmd`
*/
export function batch<A>(a: Cmd<A>): Cmd<A>;
export function batch<A,B>(a: Cmd<A>, b: Cmd<B>): Cmd<A|B>;
export function batch<A,B,C>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>): Cmd<A|B|C>;
export function batch<A,B,C,D>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>): Cmd<A|B|C|D>;
export function batch<A,B,C,D,E>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>): Cmd<A|B|C|D|E>;
export function batch<A,B,C,D,E,F>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>, f: Cmd<F>): Cmd<A|B|C|D|E|F>;
export function batch<A,B,C,D,E,F,G>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>, f: Cmd<F>, g: Cmd<G>): Cmd<A|B|C|D|E|F|G>;
export function batch<A,B,C,D,E,F,G,H>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>, f: Cmd<F>, g: Cmd<G>, h: Cmd<H>): Cmd<A|B|C|D|E|F|G|H>;
export function batch<A,B,C,D,E,F,G,H,I>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>, f: Cmd<F>, g: Cmd<G>, h: Cmd<H>, i: Cmd<I>): Cmd<A|B|C|D|E|F|G|H|I>;
export function batch<A,B,C,D,E,F,G,H,I,J>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>, f: Cmd<F>, g: Cmd<G>, h: Cmd<H>, i: Cmd<I>, j: Cmd<J>): Cmd<A|B|C|D|E|F|G|H|I|J>;
export function batch<array extends Cmd<any>[]>(signals: array): Cmd<array[number]['_T']['_R']>;
export function batch(): Cmd<any> {
const observables = Array.isArray(arguments[0]) ? arguments[0] : arguments;
return Rx.merge.apply(undefined, observables);
}
/**
* Объединение нескольких `Cmd` в очередь
*/
export function concat<A>(a: Cmd<A>): Cmd<A>;
export function concat<A,B>(a: Cmd<A>, b: Cmd<B>): Cmd<A|B>;
export function concat<A,B,C>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>): Cmd<A|B|C>;
export function concat<A,B,C,D>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>): Cmd<A|B|C|D>;
export function concat<A,B,C,D,E>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>): Cmd<A|B|C|D|E>;
export function concat<A,B,C,D,E,F>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>, f: Cmd<F>): Cmd<A|B|C|D|E|F>;
export function concat<A,B,C,D,E,F,G>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>, f: Cmd<F>, g: Cmd<G>): Cmd<A|B|C|D|E|F|G>;
export function concat<A,B,C,D,E,F,G,H>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>, f: Cmd<F>, g: Cmd<G>, h: Cmd<H>): Cmd<A|B|C|D|E|F|G|H>;
export function concat<A,B,C,D,E,F,G,H,I>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>, f: Cmd<F>, g: Cmd<G>, h: Cmd<H>, i: Cmd<I>): Cmd<A|B|C|D|E|F|G|H|I>;
export function concat<A,B,C,D,E,F,G,H,I,J>(a: Cmd<A>, b: Cmd<B>, c: Cmd<C>, d: Cmd<D>, e: Cmd<E>, f: Cmd<F>, g: Cmd<G>, h: Cmd<H>, i: Cmd<I>, j: Cmd<J>): Cmd<A|B|C|D|E|F|G|H|I|J>;
export function concat<array extends Cmd<any>[]>(signals: array): Cmd<array[number]['_T']['_R']>;
export function concat(): Cmd<any> {
const observables = Array.isArray(arguments[0]) ? arguments[0] : arguments;
return Rx.concat.apply(undefined, observables);
}
/**
* Примитивный `Cmd` не генерирует никаких действий, завершается сразу
* после запуска.
*/
export const noop: Cmd<never> = new Rx.Observable(observer => observer.complete());
/**
* Выполнение сайд-еффектов. Observable не генерирует событий, после
* запуска вызывается переданная функция и Observable завершается.
*/
export function forget<A>(f: () => A): Cmd<never>;
export function forget<A,B>(f: (a: A) => B, a: A): Cmd<never>;
export function forget<A,B,C>(f: (a: A, b: B) => C, a: A, b: B): Cmd<never>;
export function forget<A,B,C,D>(f: (a: A, b: B, c: C) => D, a: A, b: B, c: C): Cmd<never>;
export function forget(f: (...args: any[]) => void, ...rest: any[]): Cmd<never> {
return Observable.create(observer => (f.apply(undefined, rest), observer.complete()));
}
export * from '../either';
```sh
yarn install
./node_modules/.bin/ts-node ./eff-advice.ts
```
import * as t from '../decode';
import { Either, success, failure } from '../either';
import * as readline from 'readline';
/// пример кастомного декодера
const loginDecoder = t.decoder<string>('login', value => {
if (typeof(value) !== 'string') return failure('логин должен быть строкой');
if (value.length < 3) return failure('длина логина должна быть не меньше 3 символов');
if (value.length > 9) return failure('длина логина должна быть не больше 9 символов');
if (/\d/.test(value[0])) return failure('логин не должен начинаться с цифры');
if (!/[\w_\-]+/.test(value)) return failure('логин может содержать только буквы, цифры и знаки -_');
return success(value);
});
/// можно использовать предопределенные декодеры для предварительной проверки
const passwordDecoder = t.string.chain(value => t.decoder(() => {
if (value.length < 6) return failure('длина пароля должна быть не меньше 6 символов');
if (!/\d+/.test(value)) return failure('пароль должен содержать тотябы одну цифру');
if (!/\a+/.test(value)) return failure('пароль должен содержать тотябы одну букву');
return success(value);
}));
/// простые декодеры можно комбинировать в более сложные с помошью `t.record`, `t.ap`, `t.oneOf` и тд
const formDecoder = t.record({
login: loginDecoder,
password: passwordDecoder,
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function validateJSON(str: string): Either<string, any> {
try {
return success(JSON.parse(str));
} catch {
return failure('невалидный JSON');
}
}
rl.setPrompt('Форма регистрации: введите логин и пароль в виде json строки {"login":"…", "password":"…"}\n> ');
rl.on('line', (answer) => {
console.log(validateJSON(answer).chain(x => formDecoder.validate(x)));
rl.prompt();
});
rl.prompt();
import * as eff from '../eff';
import { Eff } from '../eff';
import * as t from '../decode';
import * as http from '../http';
import { HttpError } from '../http';
import { Either } from '../either';
import Monad from 'burrido';
/// shim для nodejs
if (typeof(XMLHttpRequest) === 'undefined') global['XMLHttpRequest'] = require('xhr2');
// ---------------------------------------------------------------------------
// Пример do-нотации с еффектами.
//
// Do-нотация позволяет развернуть монадные вычисления (цепочки с
// методом `chain`) в императивный код. Синтаксис acync/await —
// частный случай do-нотации для промисов.
//
// пример do-нотации:
// const process = Do(function*(){
// const x = yield fooAction;
// const y = yield barAction;
// const z = yield bazAction;
// return `x is ${x}, y is ${y} and z is ${z}`;
// });
//
// аналогичный код с использованием `chain`
// const process = fooAction.chain(
// x => barAction.chain(
// y => bazAction.chain(
// z => of(`x is ${x}, y is ${y} and z is ${z}`)
// )
// )
// );
// ---------------------------------------------------------------------------
const EffMonad = Monad({
pure: eff.of,
bind: (xs, f) => xs.chainEff(f),
});
/// https://api.random.org/json-rpc/1/basic
function randomOrgMethod<a>(method: string, params: object, decoder: t.Decoder<a>): Eff<HttpError, a> {
return http.send({
url: 'https://api.random.org/json-rpc/1/invoke',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: { "jsonrpc": "2.0", "method": method, "params": { ...params, "apiKey": "8e5b7dd6-fe16-41f1-842d-57481c2777b0" }, "id": 42 }
}).chainEff((x => (http.expectJSON(t.at(['result', 'random', 'data'], decoder))(x))));
}
const generateIntegers = (n: number, min: number, max: number) => randomOrgMethod('generateIntegers', { n, min, max }, t.array(t.nat));
const generateStrings = (n: number, length: number, characters: string) => randomOrgMethod('generateStrings', { n, length, characters }, t.array(t.string));
const generateUUIDs = (n: number) => randomOrgMethod('generateUUIDs', { n }, t.array(t.string));
const generateElements = <a>(array: a[]) => generateIntegers(1, 0, array.length - 1).mapEff(([idx]) => array[idx]);
/// тип результата
type Result =
| { tag: 'Integers', data: number[] }
| { tag: 'Strings', data: string[] }
| { tag: 'UUIDs', data: string[] }
/// пример do-нотации, выведение типов не работает
const randomOrgEff: Eff<HttpError, Result> = EffMonad.Do(function*(){
const tag = yield generateElements(['Integers', 'Strings', 'UUIDs']);
let data = undefined;
switch (tag) {
case 'Integers': data = yield generateIntegers(10, 0, 10000); break;
case 'Strings': data = yield generateStrings(4, 10, 'abcdefjhijklmnopqrstuvwxyz'); break;
case 'UUIDs': data = yield generateUUIDs(3); break;
}
return { tag, data };
});
function resultToString(ethr: Either<HttpError, Result>): string {
if (ethr.tag === 'Left') return 'Error: ' + ethr.value.tag
switch (ethr.value.tag) {
case 'Integers': return 'Integers: ' + ethr.value.data.join(',');
case 'Strings': return 'Strings: ' + ethr.value.data.join(',');
case 'UUIDs': return 'UUIDs: ' + ethr.value.data.join(',');
}
}
/// еффект, как любой Observable можно запустить несколько раз
for (let i = 0; i < 10; i++) randomOrgEff.subscribe(ethr => console.log(`randomOrgEff #${i}:`, resultToString(ethr)));
import { Eff } from '../eff';
import * as eff from '../eff';
import { HttpError } from '../http';
import * as http from '../http';
import * as t from '../decode';
/// в отличии от других контейнеров, в `Eff` методы `map` и `chain` называются `mapEff` и `chainEff`
/// shim для nodejs
if (typeof(XMLHttpRequest) === 'undefined') global['XMLHttpRequest'] = require('xhr2');
/// создаем декодер для ответов api, api возвращает json в виде { "slip": { "advice": "…" } }
const adviceDecoder = t.at(['slip', 'advice'], t.string);
/// Eff<HttpError, string> эффект отправляет запрос к api, возвращает либо текст с советом либо `HttpError`
/// в отличии от промисов еффекты не начинают выполнение при создании
const getAdvice = http.get('http://api.adviceslip.com/advice').chainEff(http.expectJSON(adviceDecoder));
/// eff.ap выполняет переданные еффекты и применяет к результатам функцию переданную последним аргументом
const getThreeAdvices = eff.ap(getAdvice, getAdvice, getAdvice, (one, two, three) => ({ one, two, three }));
/// `chainEff` позволяет принять решение на основе результата предыдущего действия
/// getAdviceTwoHundreds отправляет запросы к api до тех пор пока суммарная длина советов не пресысит 200 символов
const loop = (acc: string[]) => getAdvice.chainEff(x => acc.reduce((len, x) => len + x.length, 0) + x.length > 200 ? eff.of(acc.concat([x])) : loop(acc.concat([x])));1
const getAdviceTwoHundreds = loop([]);
/// `Eff` — это просто алиас типа Observable<Either<error, success>> потому можно использовать любые операторы из rxjs
/// вызов `subscribe` инициирует выполнение действий
getAdvice.subscribe(ethr => console.log('getAdvice:', ethr), error => console.error(error));
getThreeAdvices.subscribe(ethr => console.log('getThreeAdvices:', ethr), error => console.error(error));
getAdviceTwoHundreds.subscribe(ethr => console.log('getAdviceTwoHundreds:', ethr), error => console.error(error));
import { Eff } from '../eff';
import * as eff from '../eff';
import { HttpError } from '../http';
import * as http from '../http';
import * as t from '../decode';
import { success, failure } from '../either';
/// в отличии от других контейнеров, в `Eff` методы `map` и `chain` называются `mapEff` и `chainEff`
/// shim для nodejs
if (typeof(XMLHttpRequest) === 'undefined') global['XMLHttpRequest'] = require('xhr2');
/// декодер пытается привести значение к числу
const toNumber = t.decoder<number>('toNumber', value => {
const result = Number(value);
return isNaN(result) ? failure('cannot coerce to number') : success(result);
});
/// inner decoder
const channelDecoder = t.record({
units: t.record({ distance: t.string, pressure: t.string, speed: t.string, temperature: t.string }),
wind: t.record({ chill: toNumber, direction: toNumber, speed: toNumber }),
atmosphere: t.record({ humidity: toNumber, pressure: toNumber, rising: toNumber, visibility: toNumber }),
item: t.record({ condition: t.record({ date: t.string, temp: toNumber, text: t.string }) }),
});
type Channel = typeof channelDecoder['_a']
/// outer decoder
const resultsDecoder = t.at(['query', 'results', 'channel'], channelDecoder);
/// https://developer.yahoo.com/weather/
function yahooWeather(query: string): Eff<HttpError, Channel> {
const q = `select units, wind, atmosphere, item.condition from weather.forecast where woeid in (select woeid from geo.places(1) where text="${query}")`;
return http.send({
url: http.join('https://query.yahooapis.com/v1/public/yql', { q, format: 'json', env: 'store://datatables.org/Falltableswithkeys' }),
method: 'GET',
}).chainEff(http.expectJSON(resultsDecoder));
}
function formatResults(city: string, channel: Channel|null) {
if (!channel) {
console.log(`City: ${city}: Error`, );
return;
}
const { atmosphere, wind } = channel;
const { condition } = channel.item;
const { distance, pressure, speed, temperature } = channel.units;
console.log(`City: ${city}, ${condition.date}`);
console.log(`\tCondition: ${condition.temp} ${temperature}, ${condition.text}`);
console.log(`\tWind: ${wind.speed} ${speed}, direction: ${wind.direction}, chill: ${wind.chill} ${temperature}`);
console.log(`\tAtmosphere: visibility: ${atmosphere.visibility} ${distance}, pressure: ${atmosphere.pressure} ${pressure}, humidity: ${atmosphere.humidity}, rising: ${atmosphere.rising}`);
}
const cities = ['Izhevsk', 'Moscow', 'London', 'Beijing'];
const cityGetWeather = city => yahooWeather(city).onError(() => eff.of(null)).mapEff(result => ({ city, result }));
const traverseCities = eff.traverse(cities, cityGetWeather);
/// вызов `subscribe` инициирует выполнение действий
traverseCities.subscribe(ethr => ethr.tag === 'Right' && ethr.value.forEach(({ city, result }, idx) => (idx !== 0 && console.log('------'), formatResults(city, result))));
{
"name": "demos",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@types/node": "^8.0.53",
"burrido": "^1.0.8",
"rxjs": "^5.5.2",
"ts-node": "^3.3.0",
"typescript": "^2.6.1",
"xhr2": "^0.1.4"
}
}
import * as r from '../router';
import * as readline from 'readline';
/// объявление парсера
const parser = r.oneOf(
// #home
r.tag('Home').path('/home'),
// #shop?search=&limit=&offset=
r.tag('Shop').path('/shop').params({
search: r.string.withDefault(''),
limit: r.nat.withDefault(100),
offset: r.nat.withDefault(0),
}),
// #shop/:id
r.tag('Shop/item').path('/shop').segment('id', r.string),
);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.setPrompt('Введите относительный url\n> ');
rl.on('line', answer => {
const maybeRoute = parser.parseUrl(answer);
if (maybeRoute.tag === 'Some') {
console.log(maybeRoute.value);
} else {
console.log('404: страница не найдена :(');
}
rl.prompt();
});
rl.prompt();
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/node@^8.0.53":
version "8.0.53"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8"
ansi-styles@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
dependencies:
color-convert "^1.9.0"
arrify@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
burrido@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/burrido/-/burrido-1.0.8.tgz#b29684a5486ab3301149147983c006933847f324"
dependencies:
immutagen "^1.0.0"
chalk@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
dependencies:
ansi-styles "^3.1.0"
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
color-convert@^1.9.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
dependencies:
color-name "^1.1.1"
color-name@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
diff@^3.1.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
has-flag@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
homedir-polyfill@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
dependencies:
parse-passwd "^1.0.0"
immutagen@^1.0.0:
version "1.0.7"
resolved "https://registry.yarnpkg.com/immutagen/-/immutagen-1.0.7.tgz#88d8f57326a421c10247ee4a02a46f47aebf13b9"
make-error@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.0.tgz#52ad3a339ccf10ce62b4040b708fe707244b8b96"
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
mkdirp@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
minimist "0.0.8"
parse-passwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
rxjs@^5.5.2:
version "5.5.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.2.tgz#28d403f0071121967f18ad665563255d54236ac3"
dependencies:
symbol-observable "^1.0.1"
source-map-support@^0.4.0:
version "0.4.18"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
dependencies:
source-map "^0.5.6"
source-map@^0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
strip-json-comments@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
supports-color@^4.0.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
dependencies:
has-flag "^2.0.0"
symbol-observable@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
ts-node@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-3.3.0.tgz#c13c6a3024e30be1180dd53038fc209289d4bf69"
dependencies:
arrify "^1.0.0"
chalk "^2.0.0"
diff "^3.1.0"
make-error "^1.1.1"
minimist "^1.2.0"
mkdirp "^0.5.1"
source-map-support "^0.4.0"
tsconfig "^6.0.0"
v8flags "^3.0.0"
yn "^2.0.0"
tsconfig@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-6.0.0.tgz#6b0e8376003d7af1864f8df8f89dd0059ffcd032"
dependencies:
strip-bom "^3.0.0"
strip-json-comments "^2.0.0"
typescript@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631"
v8flags@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.0.1.tgz#dce8fc379c17d9f2c9e9ed78d89ce00052b1b76b"
dependencies:
homedir-polyfill "^1.0.1"
xhr2@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"
yn@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"
import * as Rx from 'rxjs';
import { Eff } from './eff';
import * as eff from './eff';
import { Decoder, Problem } from '../decoder';
import { success, failure } from '../either';
/** http method */
export type Method = 'GET'|'POST'|'PUT'|'DELETE'|'PATCH';
/** request */
export interface Request {
url: string;
method: Method;
body?: any;
headers?: Record<string, string|number|undefined|null>;
withCredentials?: boolean;
timeout?: number;
}
/** raw error */
export type HttpError =
| { tag: 'BadUrl', desc: string }
| { tag: 'BadPayload', desc: string }
| { tag: 'ValidationProblem', problem: Problem, url: string }
| { tag: 'BadStatus', status: number, desc: string }
| { tag: 'Timeout' }
| { tag: 'NetworkError' }
/** responce */
export interface Response {
url: string;
status: number;
statusText: string;
headers: Record<string, string>;
body: string;
}
/** query params */
export type ParamsPrimitive = number|string|undefined|null;
export type Params = Record<string, ParamsPrimitive|ParamsPrimitive[]>;
/** progress */
export type Progress =
| { tag: 'Computable', total: number, loaded: number }
| { tag: 'Uncomputable' }
/** send a request */
export function send(req: Request): Eff<HttpError, Response> {
return Rx.Observable.create(observer => {
const xhr = new XMLHttpRequest();
xhr.addEventListener('error', () => (observer.next(failure({ tag: 'NetworkError' } as HttpError)), observer.complete()));
xhr.addEventListener('timeout', () => (observer.next(failure({ tag: 'Timeout' } as HttpError)), observer.complete()));
xhr.addEventListener('load', () => {
observer.next(success({
url: xhr.responseURL,
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders()),
body: xhr.response || xhr.responseText,
}));
observer.complete();
});
try {
xhr.open(req.method, req.url, true);
} catch (e) {
observer.next(failure({ tag: 'BadUrl', desc: req.url } as HttpError)); observer.complete();
}
//xhr.addEventListener('progress', e => onResult(success(e.lengthComputable ? { tag: 'Computable', loaded: e.loaded, total: e.total } : { tag: 'Uncomputable' })));
if (req.timeout) xhr.timeout = req.timeout;
if (typeof (req.withCredentials) !== 'undefined') xhr.withCredentials = req.withCredentials;
if (typeof (req.headers) !== 'undefined') {
for (let key in req.headers) {
if (!req.headers.hasOwnProperty(key)) continue;
const value = req.headers[key];
if (typeof(value) !== 'undefined' && value !== null)
xhr.setRequestHeader(key, String(value));
}
}
const body = Object.prototype.toString.apply(req.body) === '[object Object]' ? JSON.stringify(req.body) : req.body;
xhr.send(body);
return () => xhr.abort();
});
}
/** shortcut for GET requests */
export function get(url: string, extra?: Partial<Request>): Eff<HttpError, Response> {
return send(Object.assign({}, extra, { method: 'GET', url }) as any);
}
/** shortcut for POST requests */
export function post(url: string, extra?: Partial<Request>): Eff<HttpError, Response> {
return send(Object.assign({}, extra, { method: 'POST', url }) as any);
}
/** parse response as JSON */
export function expectJSON<A>(decoder: Decoder<A>): (resp: Response) => Eff<HttpError, A> {
return resp => {
if (resp.body === '') return eff.failure({ tag: 'BadPayload', desc: 'empty body' } as HttpError);
let val = null;
try {
val = JSON.parse(resp.body);
} catch (e) {
return eff.failure({ tag: 'BadPayload', desc: 'invalid json' } as HttpError);
}
return Rx.of(decoder.validate(val).mapLeft(problem => ({ tag: 'ValidationProblem', problem, url: resp.url } as HttpError)));
};
}
/** parse headers from string to dict */
function parseHeaders(rawHeaders: string): Record<string, string> {
const output = {};
const lines = rawHeaders.split('\r\n');
for (let i in lines) {
const index = lines[i].indexOf(': ');
if (index < 0) continue;
const key = lines[i].substring(0, index);
const value = lines[i].substring(index + 2);
output[key] = value;
}
return output;
}
/** join segments of url */
function joinTwo(a: string, b: string): string {
if (a === '') return b;
if (b === '') return a;
const trailing = a.length && a[a.length - 1] === '/';
const leading = b.length && b[0] === '/';
if (trailing && leading) return a.substring(0, a.length - 1) + b;
if (!trailing && !leading) return a + '/' + b;
return a + b;
}
/** build an url */
export function join(...args: Array<string|Params>): string {
let path = '';
let params = {} as Record<string, string>;
let query = '';
for (let i in args) {
const arg = args[i];
if (typeof (arg) === 'string') path = joinTwo(path, arg);
else Object['assign'](params, arg);
}
for (let key in params) {
if (!params.hasOwnProperty(key) || typeof(params[key]) === 'undefined' || params[key] === null) continue;
if (Array.isArray(params[key])) {
for (const v of params[key]) {
if (typeof(params[key]) === 'undefined' || params[key] === null) continue;
query += (query ? '&' : '') + `${encodeURIComponent(key)}=${encodeURIComponent(v)}`;
}
} else {
query += (query ? '&' : '') + `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`;
}
}
return query ? (path + '?' + query) : path;
}
export { Decoder } from './decode';
export { Eff } from './eff';
export { Cmd, noop, batch, concat } from './eff';
export { Either, Left, Right, success, failure } from './either';
export { Request, Response, HttpError, send } from './http';
export { Option } from './option';
export { Lens, Prism } from './optics';
export { default as Sub } from './Sub';
import * as decode from './decode';
import * as either from './either';
import * as http from './http';
import * as optics from './optics';
import * as option from './option';
import * as eff from './eff';
export { decode, either, http, optics, eff, option }
// DEPRECATED
export { eff as cmd };
/** for better type inference */
export type Expr = boolean|null|undefined|number|string|{}|any[]|[any,any]|[any,any,any]|Function
import { Either } from './either';
/** lens */
export class Lens<A,B> {
static fromProp<T, K extends keyof T>(k: K): Lens<T, T[K]> {
return new Lens((t: T) => t[k], (tk, t) => ({ ...t as any, [k as string]: tk } as any as T ));
}
constructor(readonly get: (a: A) => B, readonly set: (a: A, b: B) => A) {}
modify(a: A, f: (b: B) => B): A { return this.set(a, f(this.get(a))); }
compose<C>(that: Lens<B,C>): Lens<A,C> { return new Lens((a: A) => that.get(this.get(a)), (a: A, c: C) => this.set(a, that.set(this.get(a), c))); }
}
/** contruct a lens */
export function lens<A,B>(getter: (a: A) => B, setter: (a: A, b: B) => A): Lens<A,B> {
return new Lens(getter, setter);
}
/** prism */
export class Prism<A,B> {
constructor(readonly match: (a: A) => Either<A,B>, readonly build: (b: B) => A) {}
}
{
"name": "@bitmaster/core",
"version": "3.0.2",
"main": "index.ts",
"description": "Contains a set of methods for managing the state of applications at run-time",
"typings": "index.d.ts",
"private": true,
"repository": "ssh://g@git.bitmaster.ru:34022/npm/common.git",
"author": "Vladislav Lagunov <lagunov@taximaster2.ru>",
"license": "MIT",
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment