Effect.sync
So far we have seen how to create an Effect from a known value and how to run an Effect to obtain a value. None of this is very useful yet!
In the previous lesson our Effect produced the exact same result every time we ran it. Let’s see how we can produce more surprising, less deterministic values.
import { Effect } from 'effect'
const program = Effect.sync(() => Math.random())
const result = Effect.runSync(program)
console.log(result)
Note that our program is an Effect.Effect<number>
, just like before, but each time we run this code we get a different result.
$ npx tsx main.ts
0.5621721753001707
$ npx tsx main.ts
0.22334817968802634
We can also run our program multiple times, and get a different result each time, even within a single execution of our script.
import { Effect } from 'effect'
const program = Effect.sync(() => Math.random())
const first = Effect.runSync(program)
const second = Effect.runSync(program)
console.log(first)
console.log(second)
$ npx tsx main.ts
0.674171141081058
0.04475708662142819
This shows that our previous model was incomplete. We were thinking about an Effect as if it “holds” a value. It would be more accurate to say that the Effect data structure represents the ability to produce a value. Effect.succeed(3)
will always produce the value 3
, while Effect.sync(() => Math.random())
will produce a different random number each time it is run.
We can see the difference more clearly by putting the two side-by-side.
import { Effect } from 'effect'
const fixed = Effect.succeed(Math.random())
const fresh = Effect.sync(() => Math.random())
console.log("fixed 1:", Effect.runSync(fixed))
console.log("fixed 2:", Effect.runSync(fixed))
console.log("fixed 3:", Effect.runSync(fixed))
console.log("fresh 1:", Effect.runSync(fresh))
console.log("fresh 2:", Effect.runSync(fresh))
console.log("fresh 3:", Effect.runSync(fresh))
Note that Effect.succeed
accepts a value, while Effect.sync
accepts a function. Can you guess what the output will look like?
$ npx tsx main.ts
fixed 1: 0.3116610579652488
fixed 2: 0.3116610579652488
fixed 3: 0.3116610579652488
fresh 1: 0.8330480075151958
fresh 2: 0.5886749086067611
fresh 3: 0.6363016222991031
Note that the same “fixed” value is logged 3 times, because the random value had already been calculated. For our “fresh” Effect, we see a different result each time the program runs.
Note that Effect.sync does not accept arbitrary functions. It accepts a specific type of function called a thunk.
const sync: <A>(thunk: LazyArg<A>) => Effect.Effect<A, never, never>
Just like Effect.succeed
we are creating “an Effect of A”, but we are postponing the evaluation of the value.
Thunks are sometimes called lazy functions. A thunk is “lazy” (as opposed to “eager”) because it does not do anything until it absolutely has to. For example, what does this code print out?
import { Effect } from 'effect'
const print = Effect.sync(() => {
console.log("hello")
})
It prints nothing, because we never instructed the Effect to run. This is exactly the same as creating a function and never calling the function.
Recall that when we previously logged the Effect itself (not the result), we saw a plain object with a value. With our sync
constructor we can see that the Effect itself does not necessarily “hold” a value. Instead, it represents a way to produce values of a known type.
import { Effect } from 'effect'
const fixed = Effect.succeed(3)
const fresh = Effect.sync(() => 3)
console.log("fixed is", fixed)
console.log("fresh is", fresh)
Now we can see that an Effect is a little more complex than a wrapper around a value, and that runSync
is really “running” some code.
$ npx tsx main.ts
fixed is { _id: 'Exit', _tag: 'Success', value: 3 }
fresh is {
_id: 'Effect',
_op: 'Sync',
effect_instruction_i0: [Function (anonymous)],
effect_instruction_i1: undefined,
effect_instruction_i2: undefined
}
Our previous model of an Effect was as a container around a value, a little bit like an Array. A more accurate way of thinking about an Effect is as a data structure that represents a calculation, a little bit like a Promise.
Over the next few lessons we’ll see how Effects are similar to Promises and Arrays in some ways, but different in other important ways.
Effect.succeed
allows us to represent a value. Effect.sync
allows us to represent a function. These are the basic building blocks of a computer program. This is why we often use the variable name program
when referring to an arbitrary Effect.
Let’s recap.
- Effect-the-module contains functions for constructing Effects and for running Effects.
- Effect-the-data-type is a data type with up to 3 generic parameters.
- Effect-the-data-type is an in-memory representation of a program.
- The first parameter represents the output type of the program.
- To run the program and obtain an output value, you must explicitly run the Effect.
- You can run an Effect synchronously using Effect.runSync.
- A thunk is a synchronous function with zero parameters.
- Effect.sync creates an Effect that wraps around a thunk.