Ref
On this page
When we write programs, it is common to need to keep track of some form of state during the execution of the program. State refers to any data that can change as the program runs. For example, in a counter application, the count value changes as the user increments or decrements it. Similarly, in a banking application, the account balance changes as deposits and withdrawals are made. State management is crucial to building interactive and dynamic applications.
In traditional imperative programming, one common way to store state is using variables. However, this approach can introduce bugs, especially when the state is shared between multiple components or functions. As the program becomes more complex, managing shared state can become challenging.
To overcome these issues, Effect introduces a powerful data type called Ref
, which represents a mutable reference. With Ref
, we can share state between different parts of our program without relying on mutable variables directly. Instead, Ref
provides a controlled way to handle mutable state and safely update it in a concurrent environment.
Effect's Ref
data type enables communication between different fibers in your program. This capability is crucial in concurrent programming, where multiple tasks may need to access and update shared state simultaneously.
In this guide, we will explore how to use the Ref
data type to manage state in your programs effectively. We will cover simple examples like counting, as well as more complex scenarios where state is shared between different parts of the program. Additionally, we will show how to use Ref
in a concurrent environment, allowing multiple tasks to interact with shared state safely.
Let's dive in and see how we can leverage Ref
for effective state management in your Effect programs.
Using Ref
Let's explore how to use the Ref
data type with a simple example of a counter:
ts
import {Effect ,Ref } from "effect"export classCounter {inc :Effect .Effect <void>dec :Effect .Effect <void>get :Effect .Effect <number>constructor(privatevalue :Ref .Ref <number>) {this.inc =Ref .update (this.value , (n ) =>n + 1)this.dec =Ref .update (this.value , (n ) =>n - 1)this.get =Ref .get (this.value )}}export constmake =Effect .map (Ref .make (0), (value ) => newCounter (value ))
ts
import {Effect ,Ref } from "effect"export classCounter {inc :Effect .Effect <void>dec :Effect .Effect <void>get :Effect .Effect <number>constructor(privatevalue :Ref .Ref <number>) {this.inc =Ref .update (this.value , (n ) =>n + 1)this.dec =Ref .update (this.value , (n ) =>n - 1)this.get =Ref .get (this.value )}}export constmake =Effect .map (Ref .make (0), (value ) => newCounter (value ))
Here is the usage example of the Counter
:
ts
import {Effect } from "effect"import * asCounter from "./Counter"constprogram =Effect .gen (function* (_ ) {constcounter = yield*_ (Counter .make )yield*_ (counter .inc )yield*_ (counter .inc )yield*_ (counter .dec )yield*_ (counter .inc )constvalue = yield*_ (counter .get )console .log (`This counter has a value of ${value }.`)})Effect .runPromise (program )/*Output:This counter has a value of 2.*/
ts
import {Effect } from "effect"import * asCounter from "./Counter"constprogram =Effect .gen (function* (_ ) {constcounter = yield*_ (Counter .make )yield*_ (counter .inc )yield*_ (counter .inc )yield*_ (counter .dec )yield*_ (counter .inc )constvalue = yield*_ (counter .get )console .log (`This counter has a value of ${value }.`)})Effect .runPromise (program )/*Output:This counter has a value of 2.*/
All the operations on the Ref
data type are effectful. So when we are
reading from or writing to a Ref
, we are performing an effectful
operation.
Using Ref in a Concurrent Environment
We can use this counter in a concurrent environment, such as counting the number of requests in a RESTful API. For this example, let's update the counter concurrently:
ts
import {Effect } from "effect"import * asCounter from "./Counter"constprogram =Effect .gen (function* (_ ) {constcounter = yield*_ (Counter .make )constlogCounter = <R ,E ,A >(label : string,effect :Effect .Effect <A ,E ,R >) =>Effect .gen (function* (_ ) {constvalue = yield*_ (counter .get )yield*_ (Effect .log (`${label } get: ${value }`))return yield*_ (effect )})yield*_ (logCounter ("task 1",counter .inc ).pipe (Effect .zip (logCounter ("task 2",counter .inc ), {concurrent : true }),Effect .zip (logCounter ("task 3",counter .dec ), {concurrent : true }),Effect .zip (logCounter ("task 4",counter .inc ), {concurrent : true })))constvalue = yield*_ (counter .get )yield*_ (Effect .log (`This counter has a value of ${value }.`))})Effect .runPromise (program )/*Output:... fiber=#2 message="task 4 get: 0"... fiber=#4 message="task 3 get: 1"... fiber=#5 message="task 1 get: 0"... fiber=#5 message="task 2 get: 1"... fiber=#0 message="This counter has a value of 2."*/
ts
import {Effect } from "effect"import * asCounter from "./Counter"constprogram =Effect .gen (function* (_ ) {constcounter = yield*_ (Counter .make )constlogCounter = <R ,E ,A >(label : string,effect :Effect .Effect <A ,E ,R >) =>Effect .gen (function* (_ ) {constvalue = yield*_ (counter .get )yield*_ (Effect .log (`${label } get: ${value }`))return yield*_ (effect )})yield*_ (logCounter ("task 1",counter .inc ).pipe (Effect .zip (logCounter ("task 2",counter .inc ), {concurrent : true }),Effect .zip (logCounter ("task 3",counter .dec ), {concurrent : true }),Effect .zip (logCounter ("task 4",counter .inc ), {concurrent : true })))constvalue = yield*_ (counter .get )yield*_ (Effect .log (`This counter has a value of ${value }.`))})Effect .runPromise (program )/*Output:... fiber=#2 message="task 4 get: 0"... fiber=#4 message="task 3 get: 1"... fiber=#5 message="task 1 get: 0"... fiber=#5 message="task 2 get: 1"... fiber=#0 message="This counter has a value of 2."*/
Using Ref as a Service
You can also pass a Ref
as a service to share state between different parts of your program. Let's see how this works:
ts
import {Effect ,Context ,Ref } from "effect"// Create a Tag for our stateclassMyState extendsContext .Tag ("MyState")<MyState ,Ref .Ref <number>>() {}// Subprogram 1: Increment the state value twiceconstsubprogram1 =Effect .gen (function* (_ ) {conststate = yield*_ (MyState )yield*_ (Ref .update (state , (n ) =>n + 1))yield*_ (Ref .update (state , (n ) =>n + 1))})// Subprogram 2: Decrement the state value and then increment itconstsubprogram2 =Effect .gen (function* (_ ) {conststate = yield*_ (MyState )yield*_ (Ref .update (state , (n ) =>n - 1))yield*_ (Ref .update (state , (n ) =>n + 1))})// Subprogram 3: Read and log the current value of the stateconstsubprogram3 =Effect .gen (function* (_ ) {conststate = yield*_ (MyState )constvalue = yield*_ (Ref .get (state ))console .log (`MyState has a value of ${value }.`)})// Compose subprograms 1, 2, and 3 to create the main programconstprogram =Effect .gen (function* (_ ) {yield*_ (subprogram1 )yield*_ (subprogram2 )yield*_ (subprogram3 )})// Create a Ref instance with an initial value of 0constinitialState =Ref .make (0)// Provide the Ref as a serviceconstrunnable =Effect .provideServiceEffect (program ,MyState ,initialState )// Run the program and observe the outputEffect .runPromise (runnable )/*Output:MyState has a value of 2.*/
ts
import {Effect ,Context ,Ref } from "effect"// Create a Tag for our stateclassMyState extendsContext .Tag ("MyState")<MyState ,Ref .Ref <number>>() {}// Subprogram 1: Increment the state value twiceconstsubprogram1 =Effect .gen (function* (_ ) {conststate = yield*_ (MyState )yield*_ (Ref .update (state , (n ) =>n + 1))yield*_ (Ref .update (state , (n ) =>n + 1))})// Subprogram 2: Decrement the state value and then increment itconstsubprogram2 =Effect .gen (function* (_ ) {conststate = yield*_ (MyState )yield*_ (Ref .update (state , (n ) =>n - 1))yield*_ (Ref .update (state , (n ) =>n + 1))})// Subprogram 3: Read and log the current value of the stateconstsubprogram3 =Effect .gen (function* (_ ) {conststate = yield*_ (MyState )constvalue = yield*_ (Ref .get (state ))console .log (`MyState has a value of ${value }.`)})// Compose subprograms 1, 2, and 3 to create the main programconstprogram =Effect .gen (function* (_ ) {yield*_ (subprogram1 )yield*_ (subprogram2 )yield*_ (subprogram3 )})// Create a Ref instance with an initial value of 0constinitialState =Ref .make (0)// Provide the Ref as a serviceconstrunnable =Effect .provideServiceEffect (program ,MyState ,initialState )// Run the program and observe the outputEffect .runPromise (runnable )/*Output:MyState has a value of 2.*/
Note that we use Effect.provideServiceEffect
instead of Effect.provideService
to provide an actual implementation of the MyState
service because all the operations on the Ref
data type are effectful, including the creation Ref.make(0)
.
Sharing state between Fibers
Let's consider an example where we want to read names from user input until the user enters the command "q"
to exit.
First, let's introduce a readLine
utility to read user input (ensure you have @types/node
installed):
ts
import {Effect } from "effect"import * asNodeReadLine from "node:readline"export constreadLine = (message : string):Effect .Effect <string> =>Effect .promise (() =>newPromise ((resolve ) => {constrl =NodeReadLine .createInterface ({input :process .stdin ,output :process .stdout })rl .question (message , (answer ) => {rl .close ()resolve (answer )})}))
ts
import {Effect } from "effect"import * asNodeReadLine from "node:readline"export constreadLine = (message : string):Effect .Effect <string> =>Effect .promise (() =>newPromise ((resolve ) => {constrl =NodeReadLine .createInterface ({input :process .stdin ,output :process .stdout })rl .question (message , (answer ) => {rl .close ()resolve (answer )})}))
Now, let's take a look at the main program:
ts
import {Effect ,Chunk ,Ref } from "effect"import * asReadLine from "./ReadLine"constgetNames =Effect .gen (function* (_ ) {constref = yield*_ (Ref .make (Chunk .empty <string>()))while (true) {constname = yield*_ (ReadLine .readLine ("Please enter a name or `q` to exit: "))if (name === "q") {break}yield*_ (Ref .update (ref , (state ) =>Chunk .append (state ,name )))}return yield*_ (Ref .get (ref ))})Effect .runPromise (getNames ).then (console .log )/*Output:Please enter a name or `q` to exit: AlicePlease enter a name or `q` to exit: BobPlease enter a name or `q` to exit: q{_id: "Chunk",values: [ "Alice", "Bob" ]}*/
ts
import {Effect ,Chunk ,Ref } from "effect"import * asReadLine from "./ReadLine"constgetNames =Effect .gen (function* (_ ) {constref = yield*_ (Ref .make (Chunk .empty <string>()))while (true) {constname = yield*_ (ReadLine .readLine ("Please enter a name or `q` to exit: "))if (name === "q") {break}yield*_ (Ref .update (ref , (state ) =>Chunk .append (state ,name )))}return yield*_ (Ref .get (ref ))})Effect .runPromise (getNames ).then (console .log )/*Output:Please enter a name or `q` to exit: AlicePlease enter a name or `q` to exit: BobPlease enter a name or `q` to exit: q{_id: "Chunk",values: [ "Alice", "Bob" ]}*/
Now that we have learned how to use the Ref
data type, we can use it to manage the state concurrently. For example, assume while we are reading from the console, we have another fiber that is trying to update the state from a different source:
ts
import {Effect ,Chunk ,Ref ,Fiber } from "effect"import * asReadLine from "./ReadLine"constgetNames =Effect .gen (function* (_ ) {constref = yield*_ (Ref .make (Chunk .empty <string>()))constfiber1 = yield*_ (Effect .fork (Effect .gen (function* (_ ) {while (true) {constname = yield*_ (ReadLine .readLine ("Please enter a name or `q` to exit: "))if (name === "q") {break}yield*_ (Ref .update (ref , (state ) =>Chunk .append (state ,name )))}})))constfiber2 = yield*_ (Effect .fork (Effect .gen (function* (_ ) {for (constname of ["John", "Jane", "Joe", "Tom"]) {yield*_ (Ref .update (ref , (state ) =>Chunk .append (state ,name )))yield*_ (Effect .sleep ("1 seconds"))}})))yield*_ (Fiber .join (fiber1 ))yield*_ (Fiber .join (fiber2 ))return yield*_ (Ref .get (ref ))})Effect .runPromise (getNames ).then (console .log )/*Output:Please enter a name or `q` to exit: AlicePlease enter a name or `q` to exit: BobPlease enter a name or `q` to exit: q{_id: "Chunk",values: [ ... ]}*/
ts
import {Effect ,Chunk ,Ref ,Fiber } from "effect"import * asReadLine from "./ReadLine"constgetNames =Effect .gen (function* (_ ) {constref = yield*_ (Ref .make (Chunk .empty <string>()))constfiber1 = yield*_ (Effect .fork (Effect .gen (function* (_ ) {while (true) {constname = yield*_ (ReadLine .readLine ("Please enter a name or `q` to exit: "))if (name === "q") {break}yield*_ (Ref .update (ref , (state ) =>Chunk .append (state ,name )))}})))constfiber2 = yield*_ (Effect .fork (Effect .gen (function* (_ ) {for (constname of ["John", "Jane", "Joe", "Tom"]) {yield*_ (Ref .update (ref , (state ) =>Chunk .append (state ,name )))yield*_ (Effect .sleep ("1 seconds"))}})))yield*_ (Fiber .join (fiber1 ))yield*_ (Fiber .join (fiber2 ))return yield*_ (Ref .get (ref ))})Effect .runPromise (getNames ).then (console .log )/*Output:Please enter a name or `q` to exit: AlicePlease enter a name or `q` to exit: BobPlease enter a name or `q` to exit: q{_id: "Chunk",values: [ ... ]}*/