theme | background | class | highlighter | lineNumbers | info | drawings | css | |
---|---|---|---|---|---|---|---|---|
default |
text-center |
shiki |
false |
## Antoine Coulon, Effect Paris # 3
Les Fibers décryptées : la force cachée derrière Effect
|
|
unocss |
Lead Software Engineer @ evryg
Créateur skott
Auteur effect-introduction
Advocate Effect
Contributor Rush.js, NodeSecure
-
The nature of an Effect
-
Effect runtime fundamentals
-
What is a Fiber?
const promise = Promise.resolve(1)
const effect = Effect.succeed(1)
const promise = new Promise((resolve) => {
console.log('Promise')
resolve(1)
})
const effect = Effect.sync(() => {
console.log('Effect')
return 1
})
A Promise is:
- eagerly evaluated
- represents arunning operation
An Effect is:
- lazily evaluated
- represents the description of an operation
- is animmutable data structure
Concept of Programs as Values whose goal is to distinguish:
- the description of a program (being)
- from its interpretation (doing)
Description (being) ```ts const program = Effect.sync(() => 1)
console.log(program)
```json
{
_id: 'Effect',
_op: 'Sync',
effect_instruction_i0: [Function (anonymous)],
effect_instruction_i1: undefined,
effect_instruction_i2: undefined
}
Effect is aDomain Specific Language
-
Embedded, meaning that it integrates with a host language, in this case TypeScript
-
Uses aninitial encoding, meaning the description is separated from the interpretation, allowing multiple interpreters to be used for the same description
// Décrit avec TypeScript
const program = Effect.sync(() => {})
// Dont l'interprétation est déléguée
interpreter1.run(program)
interpreter2.run(program)
// interpreter3 etc...
import { Effect } from "effect"
const program = Effect.sync(() => {})
const primitive = {
_id: "Effect",
_op: "Sync",
effect_instruction_i0: Function,
effect_instruction_i1: undefined,
effect_instruction_i2: undefined,
}
export const DumbRuntime = {
runSync: <A, E>(program: Effect.Effect<A, E, never>) => {
const effect = program as {
_op: "Sync";
effect_instruction_i0: () => unknown;
};
switch (effect._op) {
case "Sync": {
return effect.effect_instruction_i0()
}
}
}
}
cur = this.currentTracer.context(() => {
// [...]
return this[(cur as core.Primitive)._op](cur as core.Primitive);
}, this);
/** @internal */
export type Primitive =
| Async | Commit
| Failure | OnFailure
| OnSuccess | OnStep
| OnSuccessAndFailure | Success | Sync
| UpdateRuntimeFlags | While | WithRuntime | Yield
| OpTag | Blocked | RunBlocked | Either.Either<any, any>
| Option.Option<any>
- Concurrency
- Resource Safety
- Stack Safety
- Error management
- Performance
- etc.
A Fiber is an execution unit of a program, that is:
- Lightweight: similar to a virtual/green thread, low handling cost
- Non-blocking: designed to effectively manage competition (cooperative multitasking)
- Is responsible of executing one or more Effects during its life cycle
- Low-level: mainly orchestrated via high-level operators, but can be directly controlled with the Fiber module
- Stateful: started, suspended, interrupted
import { Effect } from "effect"
const log = Effect.log(`Something happening...`)
Effect.runSync(log)
$ tsx Program.ts
> timestamp=2024-11-04T08:44:16.166Z level=INFO fiber=#0 message="Something happening..."
pipe(
Effect.log("Delayed Task"),
Effect.delay(1000),
Effect.zip(Effect.log("Immediate Task"))
)
graph LR
Root[RootFiber #0] -->|Runs| A[DelayedTask] -->|Sequential| B[Immediate Task]
timestamp=2024-11-04T09:00:59.584Z level=INFO fiber=#0 message="Delayed Task"
timestamp=2024-11-04T09:00:59.589Z level=INFO fiber=#0 message="Immediate Task"
pipe(
Effect.log("Delayed Task"),
Effect.delay(1000),
Effect.zip(Effect.log("Immediate Task"), { concurrent: true })
)
graph LR
Root[RootFiber #0] -->|Forks into Child Fiber #2| A[DelayedTask]
Root[RootFiber #0] -->|Forks into Child Fiber #3| B[Immediate Task]
timestamp=2024-11-04T09:09:08.617Z level=INFO fiber=#3 message="Immediate Task"
timestamp=2024-11-04T09:09:09.619Z level=INFO fiber=#2 message="Delayed Task"
- Conceptualizes a hierarchical model for all tasks
- Offers strong guarantees: error management, controlled scope and life cycle
- Can be more or less compared to the representation of a Process Tree of an operating system
A[Runtime] -->|Manages| B[Root Fiber] B[Root Fiber] -->|forks| C[Child Fiber A] B[Root Fiber] -->|forks| D[Child Fiber B] C -->|forks| E[Child Fiber C]
</div>
<div>
```mermaid {scale: 0.8}
graph TB
A[Operating System] --> |Manages| X
X[Root Process PID = 1] --> |Manages| Z[Zombies Process]
X --> |Forks| B[Process A]
B[Process A] --> |Manages| C[Thread A]
B[Process A] --> |Manages| D[Thread B]
- fork
- forkDaemon
- forkScoped
- forkIn
Effect.fork: forks a child fiber, linked to the life cycle of its parent fiber
const background = pipe(
Effect.log("background"),
Effect.repeat(Schedule.spaced("5 second"))
);
const foreground = pipe(
Effect.log("foreground"),
Effect.repeat(
{
times: 2,
schedule: Schedule.spaced("1 second")
}
)
);
const program = Effect.gen(function* () {
yield* Effect.fork(background);
yield* foreground.pipe(
Effect.onExit(() => Effect.log("Bye"))
);
});
program.pipe(Effect.runFork);
graph LR
Root[RootFiber #0] -->|Forks into Child Fiber #2| A[Background Task]
timestamp=2024-11-04T09:33:00.303Z level=INFO fiber=#0 message=foreground
timestamp=2024-11-04T09:33:00.305Z level=INFO fiber=#1 message=background
timestamp=2024-11-04T09:33:01.309Z level=INFO fiber=#0 message=foreground
timestamp=2024-11-04T09:33:02.314Z level=INFO fiber=#0 message=foreground
timestamp=2024-11-04T09:33:02.317Z level=INFO fiber=#0 message=Bye
Effect.forkDaemon: forks a fiber, connected to a root fiber, detached from the parent
const foreground = pipe( Effect.log("foreground"), Effect.repeat( { times: 2, schedule: Schedule.spaced("1 second") } ) );
const program = Effect.gen(function* () { yield* Effect.forkDaemon(background); yield* foreground.pipe( Effect.onExit(() => Effect.log("Bye")) ); });
program.pipe(Effect.runFork);
</div>
<div>
```mermaid {scale: 0.8}
graph LR
Root[RootFiber #0] -->|Spawns a Daemon Fiber #1| Daemon[Background Task]
Root[RootFiber #0] -->|Exits| EndOfLife[End Of Life]
GlobalScope[Global Scope Fiber] -->|Links| Daemon[Background Task]
timestamp=2024-11-04T09:50:34.039Z level=INFO fiber=#0 message=foreground
timestamp=2024-11-04T09:50:34.042Z level=INFO fiber=#1 message=background
timestamp=2024-11-04T09:50:35.046Z level=INFO fiber=#0 message=foreground
timestamp=2024-11-04T09:50:36.049Z level=INFO fiber=#0 message=foreground
timestamp=2024-11-04T09:50:36.052Z level=INFO fiber=#0 message=Bye
timestamp=2024-11-04T09:50:39.043Z level=INFO fiber=#1 message=background
Effect.forkScoped: forks a child fiber, whose lifecycle is linked to the inherited scope
const program = Effect.gen(function* () {
// ^ Effect.Effect<void, never, Scope>
yield* Effect.forkScoped(background)
yield* foreground.pipe(
Effect.onExit(() => Effect.log("Bye from Foreground"))
)
});
program.pipe(
Effect.zip(
pipe(
Effect.log('Closing scope')
Effect.delay("10 second"),
)
),
Effect.scoped,
Effect.runFork
)
graph TB
Root[RootFiber #0] -->|Exits| EndOfLife[End Of Life]
Root[RootFiber #0] -->|Forks a Child Fiber #1| ChildFiber[Background Task]
InheritedScope[Inherited Scope] -->|Links Lifecycle| ChildFiber[Background Task]
InheritedScope[Inherited Scope] -->|Closes| EndFibers[Interrupts all fibers attached]
EndFibers[Interrupts all fibers attached] -->|Interrupts| ChildFiber
timestamp=2024-11-04T10:08:03.277Z level=INFO fiber=#0 message=foreground
timestamp=2024-11-04T10:08:03.280Z level=INFO fiber=#0 message="Bye from Foreground"
timestamp=2024-11-04T10:08:06.273Z level=INFO fiber=#1 message=background
timestamp=2024-11-04T10:08:11.277Z level=INFO fiber=#1 message=background
timestamp=2024-11-04T10:08:13.286Z level=INFO fiber=#0 message="Closing scope"
Effect.forkIn: forks a child fiber, whose lifecycle is linked to the provided scope
const program = (scope: Scope.Scope) =>
Effect.gen(function* () {
yield* Effect.forkIn(scope)(background);
});
pipe(
Effect.gen(function* () {
const scope = yield* Scope.make();
yield* Effect.forkDaemon(
pipe(
Scope.close(scope, Exit.void),
Effect.zip(Effect.log("Scope closed")),
Effect.delay("3 second")
)
);
yield* program(scope);
}),
Effect.runFork
);
graph TB
Root[RootFiber #0] -->|Exits| EndOfLife[End Of Life]
Root[RootFiber #0] -->|Forks a Child Fiber #1| ChildFiber[Background Task]
CreatedScope[Injected Scope] -->|Links Lifecycle| ChildFiber[Background Task]
CreatedScope[Injected Scope] -->|Closes| EndFibers[Interrupts all fibers attached]
EndFibers[Interrupts all fibers attached] -->|Interrupts| ChildFiber
timestamp=2024-11-04T10:37:43.416Z level=INFO fiber=#2 message=background
timestamp=2024-11-04T10:37:44.422Z level=INFO fiber=#2 message=background
timestamp=2024-11-04T10:37:45.426Z level=INFO fiber=#2 message=background
timestamp=2024-11-04T10:37:46.425Z level=INFO fiber=#1 message="Scope closed"
- Effect separates description (being) from evaluation (doing)
- An Effect describes a program using a DSL which composes an immutable and lazy data structure
- The native Effect runtime is fiber-based and uses Structured Concurrency
- Fibers are orchestrated by the runtime and controlled via direct/indirect actions
-
Effect website: https://effect.website
-
Effect introduction: https://github.com/antoine-coulon/effect-introduction