Extensible Effects from Freer Monads, More Extensible Effects in typescript.
Extensible Effects are a means to describe side effects in pure code
in a composable manner. Eff<U, A>
describes an effectful computation with the
result type A
and possible side-effects listed in a union type U
.
export type Eff<U, A> =
| Pure<U, A>
| Impure<U, A>
| Chain<U, A>
;
For example, const eff: Eff<Console|State<number>, string>;
is a
computation that produces the result of type string
, but also can
make console interactions and manipulate global state along the
way. U
is a type-level list of effect labels to which the
computation has access to. This effects accumulate in typescript union
type from lists of other effects when they compose using do-notation
or through chain
method.
const eff = Eff.Do(function *() {
const nextState = yield Console.question('Enter value for new state: '); // Eff<Console, string>
yield State.set<string>(nextState); // Eff<State<string>, string>
return 'Done'; // Eff<never, string>
}); // Eff<Console|State<string>, string>
Same thing with chain
const eff = Console.question('Enter value for new state: ').chain(
answer => State.set<string>(answer).mapTo('Done')
); // Eff<Console|State<string>, string>
Effects from listings above don't perform side effects during
construction and can be used in a pure code. In order to actually
execute them and to get the result of the computation, each kind of
effect should provide function-evaluator or a «handler» for this
particular effect. By convention these functions are named with common
prefix run*
(runState
, runReader
, runWriter
, etc)
function runReader<I>(read: () => I): <U, A>(eff: Eff<U, A>) => Eff<Exclude<U, Ask<I>>, A>;
This is the type of evaluator for label Ask<I>
. If a caller provides
a way do access global context I
, then runReader
returns a
function that receives effect of type Eff<U, A> and produces another
effect without Ask<I>
in U
. All evaluators should eliminate
corresponding effect labels from U
possibly by introducing effects
of different kind. For example runConsole
from
examples/console.ts replaces Console
label by
Async
that stands for effects for asynchronous code:
function runConsole<U, A>(effect: Eff<U, A>) => Eff<Exclude<U, Console>|Async, A>;
Eventually, after calling corresponding evaluators for all effects,
when there are no effects will be left in U
, you can execute actual
side-effects and access the result by calling runEff: <A>(eff: Eff<never, A>) => A
.
declare const eff01: Eff<State, string>;
const eff02 = runState(getter, setter, modifier)(eff01); // eff02: Eff<never, string>
const result = runEff(eff02); // result: string
Some evaluators instead of Eff
may return some other type e.g. runAsync
function runAsync<A>(effect: Eff<Async, A>): Subscribe<A>;
type Subscribe<A> = (next: (x: A) => void, completed: () => void) => Canceller;
type Canceller = () => void;
Such evaluators just like runEff
should be run after all others.
So, effects are just a data structure that describes them and
evaluators are the way to handle these effects inside Eff
. This
library contains definitions for most common side effects but in pure
programming languages where this approch is originated, it's
encouraged in application code to have many other custom effects for
implementing complex functionality. See examples to see
how it's done in typescript using this library.
Imitating global state [ 01-state.ts | demo ]
const eff01 = Eff.Do(function *() {
const current: State = yield State.get();
yield State.set(current + 1);
yield State.modify(x => x * x);
return 'Done';
});
const eff02 = runState(getter, setter, modify)(eff01); // Eliminate `State` from `U` parameter
console.log(state); // => 2
const result = runEff(eff02); // Here side-effects are being executed
console.log(result); // => Done
console.log(state); // => 9
Error handling [ 02-failure.ts | demo ]
const div = (num: number, denom: number) => Eff.Do(function *() {
if (denom === 0) yield Failure.create<Err>('Division by zero');
return num / denom;
});
const eff01 = runFailure(div(10, 5)); // Eliminate `Failure` from `U` parameter
const result01 = runEff(eff01); // Here side-effects are being executed
console.log(result01); // => Right { value: 2 }
const eff02 = runFailure(div(10, 0)); // Eliminate `Failure` from `U` parameter
const result02 = runEff(eff02); // Here side-effects are being executed
console.log(result02); // => Left { value: "Division by zero" }
Asynchronous computations [ 03-async.ts | demo ]
const eff01 = Eff.Do(function *() {
const first: number[] = yield randomOrgInts(1, 0, 10);
const second: number[] = yield randomOrgInts(1, 0, 10);
const third: number[] = yield randomOrgInts(1, 0, 10);
return [...first, ...second, ...third];
});
const eff02 = runFailure(eff01); // Eliminate `Failure` from `U` parameter
const subscribe = runAsync(eff02); // Eliminate `Async` from `U` parameter
subscribe(console.log, () => console.log('completed'));
Console interactions [ 04-console.ts | demo ]
const interaction = Eff.Do(function *() {
const first: number = yield ask(num)('Enter the first number: ');
const infix = yield ask(op)('Choose binary operation ("+", "-", "*", "/"): ')
const second: number = yield ask(num)('Enter the second number: ')
const expr = `${first} ${infix} ${second}`;
yield Console.putStrLn(`${expr} = ${eval('(' + expr + ')')}`);
const yesNo = yield ask(yn)('Try again? Y/N/y/n: ')
if (yesNo === 'Y') yield eval('interaction') as Eff<never, void>; // Should be just `interaction`
});
const eff02 = Console.runConsole(interaction);
const subscribe = runAsync(eff02);
subscribe(() => {}, () => (console.log('bye...'), process && process.exit && process.exit()));
- Add examples with ReactJS
- Find a simpler way for declaring evaluators
- Add benchmarks
- Replace Chain with FTCQueue
- https://github.com/pelotom/burrido — Do-notation in JavaScript
- https://github.com/lexi-lambda/freer-simple — A friendly effect system for Haskell
- Freer Monads, More Extensible Effects — Original paper