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 asnull
and only works withtry
/catch
statements, the next generationgoto
-
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
});
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)