Inline and pure functions
On this page
A function's declaration can promise more than its types. inline promises that its body is written into each caller, which makes a small function free to call. pure promises that it computes its result from its arguments alone, which makes it easy to reason about, test and run at compile time. This page covers both; afterwards you can choose when a function is worth marking. Here is a function small enough that a call would cost more than its work:
mod inline1;
import stdio use *;
// Small enough that a call would cost more than the work:
// its body is written into each caller instead
fn clamp(x i32, low i32, high i32) i32 inline {
if x < low { low; } elif x > high { high; } else { x; }
}
fn main() {
each reading in -20 .. 140 by 40 {
print <- reading, " becomes ", clamp(reading, 0, 100), "\n";
}
}
It prints:
-20 becomes 0 20 becomes 20 60 becomes 60 100 becomes 100 140 becomes 100
Inline functions#
inline is written after a function's return type. Wherever an inline function or method is called, its logic is generated within the caller's. For a small function that can be a real gain, not only because the call and the return disappear, but more because the code stays together, which the processor's instruction cache rewards.
The price is that an inline function has no code of its own, so a reference to one cannot be borrowed:
let f = &clamp; // **ERROR** 1083: May not borrow a reference to an inline function. Its body is
// copied into each caller, so it has no code of its own to point at.
For the same reason an inline body cannot be extern: a package that offers an inline function writes its body in full in its include file, so each program can copy it in (Calling C).
Pure functions#
Note: Not built yet. pure is not yet accepted, and the related @initpure is accepted but not checked.
Sometimes it is valuable to know that a function's only work is calculating its result from the information passed to it. Such a function is pure, and is marked so. A pure function may not:
- Access global, mutable variables, except to use regions to allocate memory.
- Call functions or methods that are not pure. This in effect prevents it from doing any I/O, since that needs functions that will never be marked pure.
pure fn factorial(var n i32) i32 { // Not built yet
if n<=1 {
return 1;
}
var result = 1;
while n-- > 1 {
result *= n;
}
result;
}
Marking a function pure makes code that uses it easier to understand, debug and test, because a reader can be sure that:
- passing it the same data always returns the same results;
- using it triggers no unexpected change of state (a side effect), such as I/O;
- it changes no state other than objects it has explicitly been given mutable references to.
Purity also makes guarantees that matter elsewhere: a pure function can safely calculate a constant value while the program compiles, and it is the model for what a module's initializer may do when it sets up global variables as the program starts. There the looser initpure rule applies: the function relies on no state that is not ready yet, and may still do set-up such as loading a library or opening a device, kept to what the module's own state needs.
Strong purity#
Fine-tuning: rarely needed.
The compiler enforces only the constraints listed above, which some would call a weak form of purity. A stronger form, referential transparency, is had by also ensuring that:
- none of the parameters is a mutable reference or a pointer;
- the logic never uses an operation that may not be deterministic, such as comparing pointers or reinterpreting machine-specific encodings as other types.
Only a strongly pure function can safely be memoized, its results remembered and reused.
You can now make a small function free to call, and know what purity will promise. Next, the targets a Cone program runs on, and how code can choose by target.