In long-running applications, managing resources efficiently is essential, particularly when building large-scale systems. If resources like socket connections, database connections, or file descriptors are not properly managed, it can lead to resource leaks, which degrade application performance and reliability. Effect provides constructs that help ensure resources are properly managed and released, even in cases where exceptions occur.
By ensuring that every time a resource is acquired, there is a corresponding mechanism to release it, Effect simplifies the process of resource management in your application.
The Scope Data Type
The Scope data type is a core construct in Effect for managing resources in a safe and composable way.
A scope represents the lifetime of one or more resources. When the scope is closed, all the resources within it are released, ensuring that no resources are leaked. Scopes also allow the addition of finalizers, which define how to release resources.
With the Scope data type, you can:
Add finalizers: A finalizer specifies the cleanup logic for a resource.
Close the scope: When the scope is closed, all resources are released, and the finalizers are executed.
Example (Managing a Scope)
In the above example, finalizers are added to the scope, and when the scope is closed, the finalizers are executed in the reverse order.
This reverse order is important because it ensures that resources are released in the correct sequence.
For instance, if you acquire a network connection and then access a file on a remote server, the file must be closed before the network connection to avoid errors.
addFinalizer
The Effect.addFinalizer function is a high-level API that allows you to add finalizers to the scope of an effect. A finalizer is a piece of code that is guaranteed to run when the associated scope is closed. The behavior of the finalizer can vary based on the Exit value, which represents how the scope was closed—whether successfully or with an error.
In this example, we use Effect.addFinalizer to add a finalizer that logs the exit state after the scope is closed. The finalizer will execute when the effect finishes, and it will log whether the effect completed successfully or failed.
The type signature:
shows that the workflow requires a Scope to run. You can provide this Scope using the Effect.scoped function, which creates a new scope, runs the effect within it, and ensures the finalizers are executed when the scope is closed.
In this case, the finalizer is executed even when the effect fails. The log output reflects that the finalizer runs after the failure, and it logs the failure details.
This example shows how a finalizer behaves when the effect is interrupted. The finalizer runs after the interruption, and the exit status reflects that the effect was stopped mid-execution.
Manually Create and Close Scopes
When you’re working with multiple scoped resources within a single operation, it’s important to understand how their scopes interact.
By default, these scopes are merged into one, but you can have more fine-grained control over when each scope is closed by manually creating and closing them.
Let’s start by looking at how scopes are merged by default:
Example (Merging Scopes)
In this case, the scopes of task1 and task2 are merged into a single scope, and when the program is run, it outputs the tasks and their finalizers in a specific order.
If you want more control over when each scope is closed, you can manually create and close them:
Example (Manually Creating and Closing Scopes)
In this example, we create two separate scopes, scope1 and scope2, and extend the scope of each task into its respective scope. When you run the program, it outputs the tasks and their finalizers in a different order.
You might wonder what happens when a scope is closed, but a task within that scope hasn’t completed yet.
The key point to note is that the scope closing doesn’t force the task to be interrupted.
Example (Closing a Scope with Pending Tasks)
Defining Resources
acquireRelease
The Effect.acquireRelease(acquire, release) function allows you to define resources that are acquired and safely released when they are no longer needed. This is useful for managing resources such as file handles, database connections, or network sockets.
To use Effect.acquireRelease, you need to define three actions:
Acquiring the Resource: An effect describing the acquisition of the resource, e.g., opening a file or establishing a database connection.
Using the Resource: An effect describing the actual process to produce a result, e.g., reading from the file or querying the database.
Releasing the Resource: The clean-up effect that ensures the resource is properly released, e.g., closing the file or the connection.
The acquisition process is uninterruptible to ensure that partial resource acquisition doesn’t leave your system in an inconsistent state.
The Effect.acquireRelease function guarantees that once a resource is successfully acquired, its release step is always executed when the Scope is closed.
Example (Defining a Simple Resource)
In the code above, the Effect.acquireRelease function creates a resource workflow that requires a Scope:
This means that the workflow needs a Scope to run, and the resource will automatically be released when the scope is closed.
You can now use the resource by chaining operations using Effect.andThen or similar functions.
We can continue working with the resource for as long as we want by using Effect.andThen or other Effect operators. For example, here’s how we can read the contents:
Example (Using the Resource)
To ensure proper resource management, the Scope should be closed when you’re done with the resource. The Effect.scoped function handles this for you by creating a Scope, running the effect, and then closing the Scope when the effect finishes.
Example (Providing the Scope with Effect.scoped)
acquireUseRelease
The Effect.acquireUseRelease function is a specialized version of the Effect.acquireRelease function that simplifies resource management by automatically handling the scoping of resources.
The main difference is that Effect.acquireUseRelease eliminates the need to manually call Effect.scoped to manage the resource’s scope. It has additional knowledge about when you are done using the resource created with the acquire step. This is achieved by providing a use argument, which represents the function that operates on the acquired resource. As a result, Effect.acquireUseRelease can automatically determine when it should execute the release step.
Example (Automatically Managing Resource Lifetime)