Side-effects
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 const or final, 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 mutable modifier,
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); } }
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 copies count elements of srcList starting at srcIdx to dstList starting at dstIdx. If the two lists are the same, then the copy might overwrite some of the elements that it reads. For example, a call like copy(length(x) − 1, x, 0, x, 1) would be brittle, because both srcList and dstList are aliased to x, and because dstList is 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 (print is 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; }
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−−;, since x is 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 either foo or bar has 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 output clauses
must not be used elsewhere in the same output clause.
In the following example, the values of a and b are
undefined, as the evaluation order is undefined in C++:
The undefined behavior might include unexpected output from an operator if there are undefined references. This side-effect affects only SPL output clauses where a value is written more than once, or written and read in different parts of the same output clause.stream<int32 a, int32 b> A = Beacon() { logic state : mutable int32 i = 0; param iterations : 10000; output A : a = i++, b = i++; }To resolve this side-effect, rewrite the output clause to remove the undefined behavior. To resolve for the Beacon operator, use the IterationCount() custom output function in the output clause, or use a logic onProcess clause 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.