The Effect System
Description of the core effect system

Effect Type & Aliases

The main type you will be working with is:
1
type Effect<S, R, E, A>
2
3
type Async<A> = Effect<unknown, unknown, never, A>;
4
type AsyncE<E, A> = Effect<unknown, unknown, E, A>;
5
type AsyncR<R, A> = Effect<unknown, R, never, A>;
6
type AsyncRE<R, E, A> = Effect<unknown, R, E, A>;
7
8
type Sync<A> = Effect<never, unknown, never, A>;
9
type SyncE<E, A> = Effect<never, unknown, E, A>;
10
type SyncR<R, A> = Effect<never, R, never, A>;
11
type SyncRE<R, E, A> = Effect<never, R, E, A>;
Copied!
The Effect signature reads as follows:
1
Effect<S, R, E, A> is an effectful computation that
2
3
can be Synchronous or Asynchronous (S) and
4
requires an environment of type R to run and
5
can produce either an error of type E or
6
a success reponse of type A
Copied!

Package Exports

Below the lost of exported modules included in @matechs/aio
1
import * as I from "io-ts";
2
import * as IT from "io-ts-types";
3
import * as NT from "newtype-ts";
4
import * as MN from "monocle-ts";
5
import * as MO from "./morphic";
6
7
export {
8
I, // io-ts
9
IT, // io-ts-types
10
NT, // newtype-ts
11
MN, // monocle-ts
12
MO // morphic-ts
13
};
14
15
export {
16
A, // fp-ts Array
17
CRef, // Concurrent Reference
18
E, // fp-ts augumented Either
19
Ex, // Exit
20
F, // fp-ts Function
21
M, // Managed
22
NEA, // fp-ts NonEmptyArray
23
O, // fp-ts augumented Option
24
Q, // Queue
25
RT, // Retry
26
Rec, // Recursion Schemes
27
Ref, // Reference
28
S, // Stream
29
SE, // StreamEither
30
Sem, // Semaphore
31
Service, // FreeEnv Service Definition & Derivation
32
T, // Effect
33
U, // Type Utils
34
combineProviders, // Combine Providers
35
eq, // fp-ts Eq
36
flow, // fp-ts flow
37
flowF, // fluent flow - not limited to 10
38
magma, // fp-ts Magma
39
map, // fp-ts Map
40
monoid, // fp-ts Monoid
41
pipe, // fp-ts pipe
42
pipeF, // fluent pipe - not limited to 10
43
record, // fp-ts Record
44
semigroup, // fp-ts Semigroup
45
set, // fp-ts Set
46
show, // fp-ts Show
47
tree // fp-ts Tree
48
} from "@matechs/prelude";
49
Copied!

Simple Effect

Let's start with a simple synchronous computation:
1
import { T, pipe, Ex } from "@matechs/aio";
2
import * as assert from "assert";
3
4
const add = (x: number, y: number): T.Sync<number> => T.sync(() => x + y);
5
const mul = (x: number, y: number): T.Sync<number> => T.sync(() => x * y);
6
7
const addAndMul = pipe(
8
add(1, 2),
9
T.chain((n) => mul(n, 2))
10
);
11
12
const result: Ex.Exit<never, number> = T.runSync(addAndMul);
13
14
assert.deepStrictEqual(result, Ex.done(6));
Copied!
The same computation can run in different ways:
1
import { T, pipe, Ex, F } from "@matechs/aio";
2
import * as assert from "assert";
3
4
const add = (x: number, y: number): T.Sync<number> => T.sync(() => x + y);
5
const mul = (x: number, y: number): T.Sync<number> => T.sync(() => x * y);
6
7
const addAndMul = pipe(
8
add(1, 2),
9
T.chain((n) => mul(n, 2))
10
);
11
12
// run as non failable promise returning Exit
13
T.runToPromiseExit(addAndMul).then((result) => {
14
assert.deepStrictEqual(result, Ex.done(6));
15
});
16
17
// run as failable promise returning result
18
T.runToPromise(addAndMul)
19
.then((result) => {
20
assert.deepStrictEqual(result, 6);
21
})
22
.catch((error) => {
23
console.error(error);
24
});
25
26
// invoking canceller cancel the computation (not in this case because all sync)
27
const canceller: F.Lazy<void> = T.run(addAndMul, (result) => {
28
assert.deepStrictEqual(result, Ex.done(6));
29
})
30
31
// run as throwable
32
const result_n: number = T.runUnsafeSync(addAndMul)
33
34
assert.deepStrictEqual(result_n, 6);
Copied!
Let's add some asynchronousity to the computation by adding a simple delay via liftDelay:
1
import { T, pipe, Ex, F } from "@matechs/aio";
2
import * as assert from "assert";
3
4
const add = (x: number, y: number): T.Sync<number> => T.sync(() => x + y);
5
const mul = (x: number, y: number): T.Sync<number> => T.sync(() => x * y);
6
7
const addAndMul = pipe(
8
add(1, 2),
9
T.chain((n) => mul(n, 2)),
10
T.liftDelay(100) // delay execution for 100ms
11
);
12
13
// run as non failable promise returning Exit
14
T.runToPromiseExit(addAndMul).then((result) => {
15
assert.deepStrictEqual(result, Ex.done(6));
16
});
17
18
// run as failable promise returning result
19
T.runToPromise(addAndMul)
20
.then((result) => {
21
assert.deepStrictEqual(result, 6);
22
})
23
.catch((error) => {
24
console.error(error);
25
});
26
27
// invoking canceller cancel the computation (not in this case because all sync)
28
const canceller: F.Lazy<void> = T.run(addAndMul, (result) => {
29
assert.deepStrictEqual(result, Ex.done(6));
30
})
Copied!
If we now try to use runSync we will get a compile error:
1
T.runSync(addAndMul)
2
// Argument of type 'AsyncRE<unknown, never, number>' is not assignable
3
// to parameter of type 'SyncRE<{}, never, number>'.
4
// Type 'unknown' is not assignable to type 'never'
Copied!
This is the first time we see a very important principle in statically typed functional programming, encoding logic at the type level to make errors impossible.

Environmental Effect

We can create a module that wraps the add / mul operations in the environment as follows:
1
import { T, pipe, Ex } from "@matechs/aio";
2
import * as assert from "assert";
3
4
// define a unique resource identifier
5
const CalculatorURI = "@matechs/examples/CalculatorURI";
6
7
// define the module description as an interface
8
interface Calculator {
9
// scope it using the previously defined URI
10
[CalculatorURI]: {
11
add(x: number, y: number): T.Sync<number>;
12
mul(x: number, y: number): T.Sync<number>;
13
};
14
}
15
16
// access the module from environment and expose the add function
17
const add = (x: number, y: number): T.SyncR<Calculator, number> =>
18
T.accessM(({ [CalculatorURI]: { add } }: Calculator) => add(x, y));
19
20
// access the module from environment and expose the mul function
21
const mul = (x: number, y: number): T.SyncR<Calculator, number> =>
22
T.accessM(({ [CalculatorURI]: { mul } }: Calculator) => mul(x, y));
23
24
// our program is now independent from a concrete implementation
25
const addAndMul: T.SyncR<Calculator, number> = pipe(
26
add(1, 2),
27
T.chain((n) => mul(n, 2))
28
);
29
30
// define a provider for the specific Calculator module
31
const provideCalculator = T.provide<Calculator>({
32
[CalculatorURI]: {
33
add: (x, y) => T.sync(() => x + y),
34
mul: (x, y) => T.sync(() => x * y)
35
}
36
});
37
38
// run the program providing the concrete implementation
39
const result: Ex.Exit<never, number> = pipe(
40
addAndMul,
41
provideCalculator,
42
T.runSync
43
);
44
45
assert.deepStrictEqual(result, Ex.done(6));
46
47
// define a second provider for the specific Calculator module
48
const provideCalculatorWithLog = (messages: Array<string>) =>
49
T.provide<Calculator>({
50
[CalculatorURI]: {
51
add: (x, y) =>
52
T.applySecond(
53
T.sync(() => {
54
messages.push(`called add with ${x}, ${y}`);
55
}),
56
T.sync(() => x + y)
57
),
58
mul: (x, y) =>
59
T.applySecond(
60
T.sync(() => {
61
messages.push(`called mul with ${x}, ${y}`);
62
}),
63
T.sync(() => x * y)
64
)
65
}
66
});
67
68
// run the program providing the concrete implementation
69
const messages: Array<string> = [];
70
const resultWithLog: Ex.Exit<never, number> = pipe(
71
addAndMul,
72
provideCalculatorWithLog(messages),
73
T.runSync
74
);
75
76
assert.deepStrictEqual(resultWithLog, Ex.done(6));
77
assert.deepStrictEqual(messages, [
78
"called add with 1, 2",
79
"called mul with 3, 2"
80
]);
Copied!

Multiple Environments

We can arbitrarily compose computations that require different environments as follows:
1
import { T, pipe, Ex } from "@matechs/aio";
2
import * as assert from "assert";
3
4
// define a unique resource identifier
5
const AddURI = "@matechs/examples/AddURI";
6
7
// define the module description as an interface
8
interface Add {
9
// scope it using the previously defined URI
10
[AddURI]: {
11
add(x: number, y: number): T.Sync<number>;
12
};
13
}
14
15
// define a unique resource identifier
16
const MulURI = "@matechs/examples/MulURI";
17
18
// define the module description as an interface
19
interface Mul {
20
// scope it using the previously defined URI
21
[MulURI]: {
22
mul(x: number, y: number): T.Sync<number>;
23
};
24
}
25
26
// access the module from environment and expose the add function
27
const add = (x: number, y: number): T.SyncR<Add, number> =>
28
T.accessM(({ [AddURI]: { add } }: Add) => add(x, y));
29
30
// access the module from environment and expose the mul function
31
const mul = (x: number, y: number): T.SyncR<Mul, number> =>
32
T.accessM(({ [MulURI]: { mul } }: Mul) => mul(x, y));
33
34
// our program is now independent from a concrete implementation
35
const addAndMul = pipe(
36
add(1, 2),
37
T.chain((n) => mul(n, 2))
38
);
39
40
// define a provider for the specific Add module
41
const provideAdd = T.provide<Add>({
42
[AddURI]: {
43
add: (x, y) => T.sync(() => x + y)
44
}
45
});
46
47
// define a provider for the specific Mul module
48
const provideMul = T.provide<Mul>({
49
[MulURI]: {
50
mul: (x, y) => T.sync(() => x * y)
51
}
52
});
53
54
// run the program providing the concrete implementation
55
const result: Ex.Exit<never, number> = pipe(
56
addAndMul, // T.SyncR<Mul & Add, number>
57
provideAdd, // T.SyncR<Mul, number>
58
provideMul, // T.Sync<number>
59
T.runSync
60
);
61
62
assert.deepStrictEqual(result, Ex.done(6));
Copied!
Note how we purposly omitted the type of addAndMul to show that all requirements are correctly inferred from usage, in fact if we forget to provide one dependency we will get a compilation error indicating that the dependency is missing as follows
1
// run the program providing the concrete implementation
2
const result: Ex.Exit<never, number> = pipe(
3
addAndMul,
4
provideAdd,
5
T.runSync // '[MulURI]' is missing in type '{}' but required in type 'Mul'
6
);
Copied!

Combining Providers

We can combine arbitrary providers as follows:
1
import { T, pipe, Ex, combineProviders } from "@matechs/aio";
2
import * as assert from "assert";
3
4
// define a unique resource identifier
5
const AddURI = "@matechs/examples/AddURI";
6
7
// define the module description as an interface
8
interface Add {
9
// scope it using the previously defined URI
10
[AddURI]: {
11
add(x: number, y: number): T.Sync<number>;
12
};
13
}
14
15
// define a unique resource identifier
16
const MulURI = "@matechs/examples/MulURI";
17
18
// define the module description as an interface
19
interface Mul {
20
// scope it using the previously defined URI
21
[MulURI]: {
22
mul(x: number, y: number): T.Sync<number>;
23
};
24
}
25
26
// access the module from environment and expose the add function
27
const add = (x: number, y: number): T.SyncR<Add, number> =>
28
T.accessM(({ [AddURI]: { add } }: Add) => add(x, y));
29
30
// access the module from environment and expose the mul function
31
const mul = (x: number, y: number): T.SyncR<Mul, number> =>
32
T.accessM(({ [MulURI]: { mul } }: Mul) => mul(x, y));
33
34
// our program is now independent from a concrete implementation
35
const addAndMul = pipe(
36
add(1, 2),
37
T.chain((n) => mul(n, 2))
38
);
39
40
// define a provider for the specific Add module
41
const provideAdd = T.provide<Add>({
42
[AddURI]: {
43
add: (x, y) => T.sync(() => x + y)
44
}
45
});
46
47
// define a provider for the specific Mul module
48
const provideMul = T.provide<Mul>({
49
[MulURI]: {
50
mul: (x, y) => T.sync(() => x * y)
51
}
52
});
53
54
// combine the 2 providers into a single one
55
// inferred as T.Provider<unknown, Add & Mul, never, never>
56
const provideLive = combineProviders().with(provideAdd).with(provideMul).done();
57
58
// run the program providing the concrete implementation
59
const result: Ex.Exit<never, number> = pipe(addAndMul, provideLive, T.runSync);
60
61
assert.deepStrictEqual(result, Ex.done(6));
62
Copied!

Higher Order Dependencies

Sometimes you may want to have your providers depending on other modules, you can do that as follows:
1
import { T, pipe, Ex } from "@matechs/aio";
2
import * as assert from "assert";
3
4
// define a unique resource identifier
5
const AddURI = "@matechs/examples/AddURI";
6
7
// define the module description as an interface
8
interface Add {
9
// scope it using the previously defined URI
10
[AddURI]: {
11
add(x: number, y: number): T.Sync<number>;
12
};
13
}
14
15
// define a unique resource identifier
16
const MulURI = "@matechs/examples/MulURI";
17
18
// define the module description as an interface
19
interface Mul {
20
// scope it using the previously defined URI
21
[MulURI]: {
22
mul(x: number, y: number): T.Sync<number>;
23
};
24
}
25
26
// access the module from environment and expose the add function
27
const add = (x: number, y: number): T.SyncR<Add, number> =>
28
T.accessM(({ [AddURI]: { add } }: Add) => add(x, y));
29
30
// access the module from environment and expose the mul function
31
const mul = (x: number, y: number): T.SyncR<Mul, number> =>
32
T.accessM(({ [MulURI]: { mul } }: Mul) => mul(x, y));
33
34
// our program is now independent from a concrete implementation
35
const addAndMul = pipe(
36
add(1, 2),
37
T.chain((n) => mul(n, 2))
38
);
39
40
// define a unique resource identifier
41
const LoggerURI = "@matechs/examples/LoggerURI";
42
43
// define a unique resource identifier
44
interface Logger {
45
// scope it using the previously defined URI
46
[LoggerURI]: {
47
log(message: string): T.Sync<void>;
48
};
49
}
50
51
// access logger from environment
52
const accessLogger = T.access(({ [LoggerURI]: logger }: Logger) => logger);
53
54
// define a provider for the specific Add module depending on Logger
55
const provideAdd = pipe(
56
accessLogger,
57
T.map(
58
(logger): Add => ({
59
[AddURI]: {
60
add: (x, y) =>
61
pipe(
62
T.sync(() => x + y),
63
T.chainTap((n) => logger.log(`result: ${n}`))
64
)
65
}
66
})
67
),
68
T.provideM // provide monadically
69
);
70
71
// define a provider for the specific Mul module
72
const provideMul = T.provide<Mul>({
73
[MulURI]: {
74
mul: (x, y) => T.sync(() => x * y)
75
}
76
});
77
78
// define a provider for the specific Log module
79
const provideLog = (messages: Array<string>) =>
80
T.provide<Logger>({
81
[LoggerURI]: {
82
log: (message) =>
83
T.sync(() => {
84
messages.push(message);
85
})
86
}
87
});
88
89
// run the program providing the concrete implementation
90
const messages: Array<string> = [];
91
const result: Ex.Exit<never, number> = pipe(
92
addAndMul, // T.SyncR<Mul & Add, number>
93
provideAdd, // T.SyncR<Logger & Add, number>
94
provideMul, // T.SyncR<Logger, number>
95
provideLog(messages), // T.Sync<number>
96
T.runSync
97
);
98
99
assert.deepStrictEqual(result, Ex.done(6));
100
assert.deepStrictEqual(messages, ["result: 3"]);
Copied!
Last modified 1yr ago