Controlling effects like values

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.