Side-effects
y = (x = 5) + x--; is
hard to read and brittle because it has multiple side-effects in a
single statement, so its meaning depends on the statement-internal
evaluation order. The situation is even worse if the same expression
calls multiple functions with side-effects. For example, the meaning
of y = foo(x, 5) + bar(x); not only depends on evaluation
order, but furthermore, depends on the definition of foo and bar.
For example, x might be a list, and foo and bar might
be push and pop. Statements with
multiple side-effects are not only hard to understand for a human,
but they are also hard to optimize for a compiler. In the absence
of side-effects, compilers often optimize by reordering or even parallelizing
independent code and eliminating redundant code. Fortunately, even
in imperative languages like C and Java™,
expression-internal side-effects are uncommon, and referential transparency
is common. Unfortunately, without language support, this behavior
s hard to establish in a compiler. SPL is designed to make
side-effects more explicit, and to encourage a coding style where
side-effects are less common.There are features and design decisions that curb side-effects:
- Mutable composite data is never aliased. Since SPL has no pointer type (see topic Composite types), and since assignments make a deep-copy even in the case of composite types (see topic Value semantics), there is no aliasing inside of composite data. That way, a side-effect to one composite variable does not silently corrupt another composite variable.
- Variables are immutable by default (see topic Statements). C++ and Java™ allow you to explicitly declare variables
immutable with
constorfinal, but even though most variables are immutable and can be declared that way, programmers typically forget to make that explicit. SPL inverts the default, making mutable an explicit modifier. Variables without that modifier are deeply immutable. That way, side-effect freedom is more common and easier to establish for humans and compilers alike. - Collections in for-loops are immutable (see topic Statements). While a for-loop iterates over a collection, that collection becomes immutable. That prevents common mistakes where the loop body has an unintended side-effect on the loop control.
In addition, SPL has the following rules to curb side-effects:
- Function parameters are immutable by default. In practice, functions that mutate their
parameters are infrequent. They are mostly used to make a small modification to a large data
structure. In SPL, mutable parameters must be explicitly annotated with the
mutablemodifier, and all other parameters are deep-immutable. Thanks to this information, the compiler can produce helpful errors and even perform optimizations. For example:void test(float64 x, list<float64> z) { for (float64 y in z) { print(x); print((x * 100.0) / y); } }
print does not modify x, a compiler can hoist
the loop-invariant expression x * 100.0 out of the
loop:void test(float64 x, list<float64> z) {
float64 loopInvariantTmp = x * 100.0;
for (float64 y in z) {
print(x);
print(loopInvariantTmp / y);
}
}Besides enabling optimizations, making function parameters immutable by default also makes code easier to read and maintain.
- Mutable function parameters are never aliased. One potential loop-hole
in the aliasing prevention that is described so far can occur when
the same data is passed to multiple function parameters. Consider
for example a function
copy(count, srcList, srcIdx, mutable dstList, dstIdx)that copiescountelements ofsrcListstarting atsrcIdxtodstListstarting atdstIdx. If the two lists are the same, then the copy might overwrite some of the elements that it reads. For example, a call likecopy(length(x) - 1, x, 0, x, 1)would be brittle, because bothsrcListanddstListare aliased tox, and becausedstListis mutable. Therefore, SPL disallows any mutable parameter to be aliased with any other parameter in the same function call.
- Functions are stateless by default. A stateful function is a function that is not referentially
transparent or has side-effects. A function is not referentially transparent if it does not
consistently yield the same result each time it is called with the same inputs. A function
has side-effects if it modifies state observable outside the function. For the purposes of
this definition, “state observable outside the function” includes global
variables in native code, and I/O to the console, files, the network, and so on, but
excludes mutable parameters. Mutable parameters are handled separately because, as the loop
invariant code motion example shows, they have separate optimization opportunities
(
printis stateful but its parameter can be hoisted). Here is an example that illustrates how code that uses stateless functions is easier to understand and optimize:int32 ackermann(int32 m, int32 n) { /* do something expensive */ return 0; } int32 test(int32 m, int32 n) { int32 x = ackermann(m, n); int32 y = ackermann(m, n); return x + y; }
ackermann function is stateless and has immutable parameters, then a
compiler might eliminate one of the
calls:int32 ackermann(int32 m, int32 n) { /* do something expensive */ return 0; }
int32 test(int32 m, int32 n) {
int32 x = ackermann(m, n);
int32 y = x;
return x + y;
}- State that is written by a statement must not be used elsewhere
in the same statement. Refer to the examples from previous topics.
This rule disallows code like
y = (x = 5) + x--;, sincexis written in one part and used in another part of the statement. The various rules that are related to functions also enable the SPL compiler to check this rule for statements that involve function calls. For example,y = foo(x, 5) + bar(x);is not allowed if eitherfooorbarhas a mutable parameter. This restriction makes code more readable, prevents common programming mistakes, and might lead to more optimization opportunities. - Values in expressions in SPL
outputclauses must not be used elsewhere in the sameoutputclause. In the following example, the values ofaandbare undefined, as the evaluation order is undefined in C++:stream<int32 a, int32 b> A = Beacon() { logic state : mutable int32 i = 0; param iterations : 10000; output A : a = i++, b = i++; }The undefined behavior might include unexpected output from an operator if there are undefined references. This side-effect affects only SPLoutputclauses where a value is written more than once, or written and read in different parts of the sameoutputclause.To resolve this side-effect, rewrite the
outputclause to remove the undefined behavior. To resolve for the Beacon operator, use the IterationCount() custom output function in theoutputclause, or use a logiconProcessclause in a Custom operator to replace the Beacon operator.
Together, these rules mean that for most statements, the compiler is free to implement any internal expression evaluation order, and the user cannot observe the difference. The only exception is expressions that involve floating point numbers, which the compiler must always implement such that they evaluate left-to-right.