DEV Community

Iven Marquardt
Iven Marquardt

Posted on

The three kinds of effects in programming

Effects can be distinguished in three categories:

  • control flow effects
  • mutations
  • input/output

Control Flow

Control flow effects have their manifestations in data types:

  • indeterministic control flow: Array/List
  • computation without a result value: null
  • unavoidable exception: Error
  • short circuit: break/continue

The problem with imperative programming is that these data types are partly insufficient to encode their corresponding control flow:

  • Array provides an indeterministic choice but if this choice changes, you need to mutate the array or endure a performance penalty
  • null merely indicates the lack of a result value but offers no means to avoid further processing
  • Error entails the same problems as null and only works with try/catch statements, the next generation goto
  • break/continue are no expressions but special nullary operators tied to their special constructs

From a functional perspective, these types are more or less harmful. FP uses algebraic data types, continuations and monads to solve shortcomings in the imperative world.

Mutations

Mutations alter existing program state observably and destructively during the runtime of a program.

They are a side effect that puts a constraint on evaluation order, i.e. order matters even when it doesn't have to. It also hampers idempotent functions. This may lead to non-intuitive program behavior and renders optimization/refactoring harder.

Functional programming uses tree structures to realize persistent data types with structural sharing.

I/O

You cannot ignore the real world, even with functional programming. If you rely on input and output and you do if your program should to achieve something useful, side effects are inevitable.

The trick is to separate the pure description of effects from their execution:

const Cont = k => {
  const o = {run: k};
  return o;
};

Cont.map = f => tx =>
  Cont(k => tx.run(x => k(f(x))));

Cont.and = tx => ty =>
  Cont(k =>
    tx.run(x =>
      ty.run(y =>
        k([x, y]))));

const sqr = x => x * x;

// database mocks

const tw = Cont(k => new Promise((res, rej) => res(k(3)))),
  tx = Cont(k => new Promise((res, rej) => res(k(5))));

const ty = Cont.map(sqr) (tw),
  tz = Cont.map(sqr) (tx);

const data = Cont.and(ty, tz);

// ^^^ pure ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

data.run(data => { // data yields [9, 25]
  // impure scope
});
Enter fullscreen mode Exit fullscreen mode

Only in the impure scope you can observe the result value of running the effect. Everything else is pure.

Pure and impure scopes are separated and effect execution is deferred by conducting it inside the last continuation of the program, namely data => { // impure scope }.

Cont builds up a nested deferred function call tree. Functional programming thus only utilizes tree data structures and continuations to handle all three effect classes.

Top comments (0)