Introduction to Runtime
On this page
The Runtime<R>
data type represents a Runtime System that can execute effects. To execute any effect, we need a Runtime
that includes the necessary requirements for that effect.
A Runtime<R>
consists of three main components:
- a value of type
Context<R>
- a value of type
FiberRefs
- a value of type
RuntimeFlags
The Default Runtime
When we use functions like Effect.run*
, we are actually using the default runtime without explicitly mentioning it. These functions are designed as convenient shortcuts for executing our effects using the default runtime.
For instance, in the Runtime
module, there is a corresponding Runtime.run*(defaultRuntime)
function that is called internally by Effect.run*
, e.g. Effect.runSync
is simply an alias for Runtime.runSync(defaultRuntime)
.
The default runtime includes:
- An empty
Context<never>
- A set of
FiberRefs
that include the default services - A default configuration for
RuntimeFlags
that enablesInterruption
andCooperativeYielding
In most cases, using the default runtime is sufficient. However, it can be useful to create a custom runtime to reuse a specific context or configuration. It is common to create a Runtime<R>
by initializing a Layer<R, Err, RIn>
. This allows for context reuse across execution boundaries, such as in a React App or when executing operations on a server in response to API requests.
What is a Runtime System?
When we write an Effect program, we construct an Effect
using constructors and combinators. Essentially, we are creating a blueprint of a program. An Effect
is merely a data structure that describes the execution of a concurrent program. It represents a tree-like structure that combines various primitives to define what the Effect
should do.
However, this data structure itself does not perform any actions; it is solely a description of a concurrent program.
Therefore, it is crucial to understand that when working with a functional effect system like Effect, our code for actions such as printing to the console, reading files, or querying databases is actually building a workflow or blueprint for an application. We are constructing a data structure.
So how does Effect actually run these workflows? This is where the Effect Runtime System comes into play. When we invoke a Runtime.run*
function, the Runtime System takes over. First, it creates an empty root Fiber with:
- The initial context
- The initial fiberRefs
- The initial Effect
After the creation of the Fiber, it invokes the Fiber's runLoop, which follows the instructions described by the Effect
and executes them step by step.
To simplify, we can envision the Runtime System as a black box that takes both the effect (Effect<A, E, R>
) and its associated context (Context<R>
). It runs the effect and returns the result as an Exit<A, E>
value.
Responsibilities of the Runtime System
Runtime Systems have a lot of responsibilities:
-
Execute every step of the blueprint. They have to execute every step of the blueprint in a while loop until it's done.
-
Handle unexpected errors. They have to handle unexpected errors, not just the expected ones but also the unexpected ones.
-
Spawn concurrent fiber. They are actually responsible for the concurrency that effect systems have. They have to spawn a fiber every time we call
fork
on an effect to spawn off a new fiber. -
Cooperatively yield to other fibers. They have to cooperatively yield to other fibers so that fibers that are sort of hogging the spotlight, don't get to monopolize all the CPU resources.
-
Ensure finalizers are run appropriately. They have to ensure finalizers are run appropriately at the right point in all circumstances to make sure that resources are closed that clean-up logic is executed. This is the feature that powers
Scope
and all the other resource-safe constructs in Effect. -
Handle asynchronous callback. They have to handle this messy job of dealing with asynchronous callbacks. So we don't have to deal with async code. When we are using Effect, everything can be interpreted as async or sync out of the box.
Default Runtime
Effect provides a default runtime named Runtime.defaultRuntime
designed for mainstream usage.
The default runtime provides the minimum capabilities to bootstrap execution of Effect tasks.
Both of the following executions are equivalent:
ts
import {Effect ,Runtime } from "effect"constprogram =Effect .log ("Application started!")Effect .runSync (program )/*Output:... level=INFO fiber=#0 message="Application started!"*/Runtime .runSync (Runtime .defaultRuntime )(program )/*Output:... level=INFO fiber=#0 message="Application started!"*/
ts
import {Effect ,Runtime } from "effect"constprogram =Effect .log ("Application started!")Effect .runSync (program )/*Output:... level=INFO fiber=#0 message="Application started!"*/Runtime .runSync (Runtime .defaultRuntime )(program )/*Output:... level=INFO fiber=#0 message="Application started!"*/
Under the hood, Effect.runSync
(and the same principle applies to other Effect.run*
functions) serves as a convenient shorthand for Runtime.runSync(Runtime.defaultRuntime)
.
Locally Scoped Runtime Configuration
In Effect, runtime configurations are typically inherited from their parent workflows. This means that when we access a runtime configuration or obtain a runtime inside a workflow, we are essentially using the configuration of the parent workflow. However, there are cases where we want to temporarily override the runtime configuration for a specific part of our code. This concept is known as locally scoped runtime configuration. Once the execution of that code region is completed, the runtime configuration reverts to its original settings.
To achieve this, we make use of Effect.provide*
functions, which allow us to provide a new runtime configuration to a specific section of our code.
Configuring Runtime by Providing Configuration Layers
By utilizing the Effect.provide
function and providing runtime configuration layers to an Effect workflow, we can easily modify runtime configurations.
Here's an example:
ts
import {Logger ,Effect } from "effect"// Define a configuration layerconstaddSimpleLogger =Logger .replace (Logger .defaultLogger ,Logger .make (({message }) =>console .log (message )))constprogram =Effect .gen (function* (_ ) {yield*_ (Effect .log ("Application started!"))yield*_ (Effect .log ("Application is about to exit!"))})Effect .runSync (program )/*Output:timestamp=... level=INFO fiber=#0 message="Application started!"timestamp=... level=INFO fiber=#0 message="Application is about to exit!"*/// Overriding the default loggerEffect .runSync (program .pipe (Effect .provide (addSimpleLogger )))/*Output:Application started!Application is about to exit!*/
ts
import {Logger ,Effect } from "effect"// Define a configuration layerconstaddSimpleLogger =Logger .replace (Logger .defaultLogger ,Logger .make (({message }) =>console .log (message )))constprogram =Effect .gen (function* (_ ) {yield*_ (Effect .log ("Application started!"))yield*_ (Effect .log ("Application is about to exit!"))})Effect .runSync (program )/*Output:timestamp=... level=INFO fiber=#0 message="Application started!"timestamp=... level=INFO fiber=#0 message="Application is about to exit!"*/// Overriding the default loggerEffect .runSync (program .pipe (Effect .provide (addSimpleLogger )))/*Output:Application started!Application is about to exit!*/
In this example, we first create a configuration layer for a simple logger using Logger.replace
.
Then, we use Effect.provide
to provide this configuration to our program, effectively overriding the default logger with the simple logger.
To ensure that the runtime configuration is only applied to a specific part of an Effect application, we should provide the configuration layer exclusively to that particular section, as demonstrated in the following example:
ts
import {Logger ,Effect } from "effect"// Define a configuration layerconstaddSimpleLogger =Logger .replace (Logger .defaultLogger ,Logger .make (({message }) =>console .log (message )))constprogram =Effect .gen (function* (_ ) {yield*_ (Effect .log ("Application started!"))yield*_ (Effect .gen (function* (_ ) {yield*_ (Effect .log ("I'm not going to be logged!"))yield*_ (Effect .log ("I will be logged by the simple logger.").pipe (Effect .provide (addSimpleLogger )))yield*_ (Effect .log ("Reset back to the previous configuration, so I won't be logged."))}).pipe (Effect .provide (Logger .remove (Logger .defaultLogger ))))yield*_ (Effect .log ("Application is about to exit!"))})Effect .runSync (program )/*Output:timestamp=... level=INFO fiber=#0 message="Application started!"I will be logged by the simple logger.timestamp=... level=INFO fiber=#0 message="Application is about to exit!"*/
ts
import {Logger ,Effect } from "effect"// Define a configuration layerconstaddSimpleLogger =Logger .replace (Logger .defaultLogger ,Logger .make (({message }) =>console .log (message )))constprogram =Effect .gen (function* (_ ) {yield*_ (Effect .log ("Application started!"))yield*_ (Effect .gen (function* (_ ) {yield*_ (Effect .log ("I'm not going to be logged!"))yield*_ (Effect .log ("I will be logged by the simple logger.").pipe (Effect .provide (addSimpleLogger )))yield*_ (Effect .log ("Reset back to the previous configuration, so I won't be logged."))}).pipe (Effect .provide (Logger .remove (Logger .defaultLogger ))))yield*_ (Effect .log ("Application is about to exit!"))})Effect .runSync (program )/*Output:timestamp=... level=INFO fiber=#0 message="Application started!"I will be logged by the simple logger.timestamp=... level=INFO fiber=#0 message="Application is about to exit!"*/
Top-level Runtime Configuration
When developing an Effect application and executing it using Effect.run*
functions, the application is automatically run using the default runtime behind the scenes. While we can adjust and customize specific aspects of our Effect application by providing locally scoped configuration layers using Effect.provide
operations, there are scenarios where we need to customize the runtime configuration for the entire application from the top level.
In such situations, we can create a top-level runtime by converting a configuration layer into a runtime using the Layer.toRuntime
operator:
ts
import {Logger ,Scope ,Layer ,Effect ,Runtime ,Exit } from "effect"// Define a configuration layerconstappLayer =Logger .replace (Logger .defaultLogger ,Logger .make (({message }) =>console .log (message )))// Create a scope for resources used in the configuration layerconstscope =Effect .runSync (Scope .make ())// Transform the configuration layer into a runtimeconstruntime = awaitEffect .runPromise (Layer .toRuntime (appLayer ).pipe (Scope .extend (scope )))// Define a custom running functionconstrunSync =Runtime .runSync (runtime )constprogram =Effect .log ("Application started!")// Execute the program using the custom runtimerunSync (program )/*Output:Application started!*/// Cleaning up any resources used by the configuration layerEffect .runFork (Scope .close (scope ,Exit .void ))
ts
import {Logger ,Scope ,Layer ,Effect ,Runtime ,Exit } from "effect"// Define a configuration layerconstappLayer =Logger .replace (Logger .defaultLogger ,Logger .make (({message }) =>console .log (message )))// Create a scope for resources used in the configuration layerconstscope =Effect .runSync (Scope .make ())// Transform the configuration layer into a runtimeconstruntime = awaitEffect .runPromise (Layer .toRuntime (appLayer ).pipe (Scope .extend (scope )))// Define a custom running functionconstrunSync =Runtime .runSync (runtime )constprogram =Effect .log ("Application started!")// Execute the program using the custom runtimerunSync (program )/*Output:Application started!*/// Cleaning up any resources used by the configuration layerEffect .runFork (Scope .close (scope ,Exit .void ))
In this example, we first create a custom configuration layer called appLayer
, which includes modifications to the logger configuration. Next, we transform this configuration layer into a runtime using Layer.toRuntime
. This results in a top-level runtime that encapsulates the desired configuration for the entire Effect application.
We then define a custom runtime function named myrunSync
using Runtime.runSync(runtime)
. Finally, we create our Effect program and execute it using the custom runtime, which applies the top-level configuration.
By customizing the top-level runtime configuration, we can tailor the behavior of our entire Effect application to meet our specific needs and requirements.
Providing Context to the Runtime System
Custom runtimes can simplify the execution of multiple effects that require the same environment. This eliminates the need to use Effect.provide*
on each individual effect before running them.
Let's break down this concept with an example. Imagine we want to create a Runtime
tailored for testing purposes, where services don't interact with real external APIs. In such cases, we can design a custom runtime specifically for testing.
Suppose we have defined two services: LoggingService
and EmailService
:
ts
import {Effect ,Context } from "effect"classLoggingService extendsContext .Tag ("LoggingService")<LoggingService ,{log : (line : string) =>Effect .Effect <void> }>() {}classEmailService extendsContext .Tag ("EmailService")<EmailService ,{send : (user : string,content : string) =>Effect .Effect <void> }>() {}
ts
import {Effect ,Context } from "effect"classLoggingService extendsContext .Tag ("LoggingService")<LoggingService ,{log : (line : string) =>Effect .Effect <void> }>() {}classEmailService extendsContext .Tag ("EmailService")<EmailService ,{send : (user : string,content : string) =>Effect .Effect <void> }>() {}
We plan to implement a live version of LoggingService
and a fake version of EmailService
for testing:
ts
import {Console } from "effect"constLoggingServiceLive =LoggingService .of ({log :Console .log })constEmailServiceFake =EmailService .of ({send : (user ,content ) =>Console .log (`sending email to ${user }: ${content }`)})
ts
import {Console } from "effect"constLoggingServiceLive =LoggingService .of ({log :Console .log })constEmailServiceFake =EmailService .of ({send : (user ,content ) =>Console .log (`sending email to ${user }: ${content }`)})
Let's create a custom runtime that includes these two service implementations within its context:
ts
import {Runtime ,FiberRefs } from "effect"consttestableRuntime =Runtime .make ({context :Context .make (LoggingService ,LoggingServiceLive ).pipe (Context .add (EmailService ,EmailServiceFake )),fiberRefs :FiberRefs .empty (),runtimeFlags :Runtime .defaultRuntimeFlags })
ts
import {Runtime ,FiberRefs } from "effect"consttestableRuntime =Runtime .make ({context :Context .make (LoggingService ,LoggingServiceLive ).pipe (Context .add (EmailService ,EmailServiceFake )),fiberRefs :FiberRefs .empty (),runtimeFlags :Runtime .defaultRuntimeFlags })
Now we can execute our effects using this custom Runtime<LoggingService | EmailService>
:
ts
constprogram =Effect .gen (function* (_ ) {const {log } = yield*_ (LoggingService )const {send } = yield*_ (EmailService )yield*_ (log ("sending newsletter"))yield*_ (send ("David", "Hi! Here is today's newsletter."))})Runtime .runPromise (testableRuntime )(program )/*Output:sending newslettersending email to David: Hi! Here is today's newsletter.*/
ts
constprogram =Effect .gen (function* (_ ) {const {log } = yield*_ (LoggingService )const {send } = yield*_ (EmailService )yield*_ (log ("sending newsletter"))yield*_ (send ("David", "Hi! Here is today's newsletter."))})Runtime .runPromise (testableRuntime )(program )/*Output:sending newslettersending email to David: Hi! Here is today's newsletter.*/
By providing context to the runtime system, we create a specialized environment for our effects, making it easier to test and control their behavior as needed.