Skip to content

Latest commit

 

History

History
634 lines (465 loc) · 14.2 KB

slides.md

File metadata and controls

634 lines (465 loc) · 14.2 KB
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
persist
unocss

Les Fibers décryptées : la force cachée derrière Effect


Antoine Coulon @ Effect Paris #3 - 05/11/2024


Antoine Coulon

Lead Software Engineer @ evryg

Créateur skott

Auteur effect-introduction

Advocate Effect

Contributor Rush.js, NodeSecure

antoine-coulon
c9antoine
LinkedIn: Antoine Coulon
dev.to/antoinecoulon
<style> h1 { color: #4c7fff; } img { margin: 0 auto; } </style>

Effect foundations


  • The nature of an Effect

  • Effect runtime fundamentals

  • What is a Fiber?


The fundamental difference between an Effect and a Promise


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
}) 
```bash $ tsx Program.ts > Promise ```

Evaluation strategy

A Promise is:

  • eagerly evaluated
  • represents arunning operation

An Effect is:

  • lazily evaluated
  • represents the description of an operation
  • is animmutable data structure

An Effect is a description of a program

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
}

The description of a TypeScript program using Effect's DSL

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...

From description to interpretation

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()
      }
    }
  }
}

What happens in the real Effect runtime?

https://github.com/Effect-TS/effect/blob/8a30e1dfa3a7103bf5414fc6a7fca3088d8c8c00/packages/effect/src/internal/fiberRuntime.ts#L1358

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>

We still have many things left to handle...


  • Concurrency
  • Resource Safety
  • Stack Safety
  • Error management
  • Performance
  • etc.

Fiber: the primitive at the heart of the Effect runtime

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

The execution of an Effect is inevitably associated to a Fiber


import { Effect } from "effect"

const log = Effect.log(`Something happening...`)

Effect.runSync(log)
A Fiber is there, running the Effect
$ tsx Program.ts
> timestamp=2024-11-04T08:44:16.166Z level=INFO fiber=#0 message="Something happening..."

Indirect use of Fibers #1

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]
Loading
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"

Indirect use of Fibers #2

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]
Loading
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"

A Fiber-based runtime governed by Structured Concurrency

  • 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
```mermaid {scale: 0.8} graph TB

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]

Direct use of Fibers


  • 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] 
Loading
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

```ts {all|17} {lines:true} 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.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
Loading
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
Loading
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 in a nutshell


  • 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

Thanks for listening!


Questions ?