Let's zoom out for a seconds. What is the problem with console.log
? Why eager execution of effects is bad? What benefits do we get from the added complexity of effect
?
Our goal is to control any effect like we do with values:
- Storing effects into variables
const value = 10; // ✅
const value = console.log("Hello"); // ⛔️
- Refactor effects without changing the meaning of the program
const main = () => {
// ...
console.log("Hello"); // ⛔️ If you move this out, `main` changes, it doesn't print anymore!
// ...
}
- Make them deterministic (easy to test)
/// ✅ Calling this with the same `n` gives always the same result
const main = (n: number) => n * 2;
/// ⛔️ How do we test this?
const main = () => {
makePayment();
}
With "effect" we mean an operation that modifies something in a system:
- Write to the database (modify the stored data)
- Opening a popup (modify the UI)
- Printing in the console (modify the console state)
const print = console.log("Hello");
print
doesn't store the logging operation. console.log
already "did" something, and it just returned nothing (void
).
It would be great instead if print
only described "something that will print on the console".
This would allow us to reuse print
, store it into an array, pass it as a function parameter, all things that statements like console.log
cannot do:
const print: Print = // image `Print` stores "something that will print on the console"
// ✅ We could store "printing" in an array
const printingArray = [print, print, print];
// ✅ We could pass "printing" to a function
const printIfTrue = (check: boolean, toPrint: Print) => {
if (check) {
toPrint.run(); // 👈 This is where we actually print on the console
}
}
// 👇 `Print` is a value
printIfTrue(true, print);
This cannot be done with console.log
:
const printingArray = [
console.log("Hello"),
console.log("Hello"),
console.log("Hello"),
];
const printIfTrue = (check: boolean, toPrint: any) => {
/// ...
};
printIfTrue(true, console.log("Hello"));
Guess what happens if you run this code?
Hello
Hello
Hello
Hello
Every time we define console.log
the print operation is executed. That's because console.log
is eager: there is no distinction between "definition" and "execution".
The moment you type console.log
printing is executed.
Image if instead of console.log
this was makePayement
😳
How to control effects
We can "control" console.log
by wrapping it into a function:
const print = () => console.log("Hello");
This makes console.log
a value. As such we can use just like we did with Print
before:
// 👇 Turn an effect into a value by wrapping it into a function
type Print = () => void;
const print: Print = () => console.log("Hello");
// ✅ We are able to treat "printing" as a value again
const printingArray = [print, print, print];
const printIfTrue = (check: boolean, toPrint: Print) => {
if (check) {
toPrint();
}
}
printIfTrue(true, print);
This works! It will print only a single Hello
caused by executing printIfTrue
:
Hello
This is critical aspect of nearly everything in effect
:
import { Console } from "effect";
const print = Console.log("Hello");
const printingArray = [print, print, print];
const printIfTrue = (check: boolean, toPrint: Effect<void>) => {
if (check) {
Effect.runSync(toPrint);
}
}
printIfTrue(true, print);
Effect
allows to control effects and treat them like values.
By working with values instead of statements we regain composition, refactoring and testing.
Once again, Effect
only describes what your program will do. You explicitly need to run it before anything happens.