Separate Calculating From Doing
Actions and Calculations
Section titled “Actions and Calculations”Decompose your code into actions (side-effects) and calculations (pure functions).
Actions depend on the ever-changing outside world. Assume the outside world is always changing, so actions depend on when you call them.
Calculations are pure and transparent, they only depend on their explicit inputs, and they produce observable outputs. Call them multiple times at any time with the same input and they will always produce the same output. Calculations depend only on their inputs.
Push the actions to the edges of the program. Read once at the start, compute with pure functions, write once at the end. What’s left in the middle needs no environment, so it can be read, reused and tested on its own.
Benefits
Section titled “Benefits”Calculations are testable
Section titled “Calculations are testable”Calculations are testable without mocks because a calculation has no dependencies to substitute. Its inputs are plain values, so a test is as simple as “call it, compare the result”. Actions, by contrast, touch the outside world, so testing them needs a test double or a real environment.
Compile time evaluation
Section titled “Compile time evaluation”Calculations can often be made constexpr in modern versions of C++, so the same test can run at compile time. Evaluate the calculation in a static_assert and a wrong result — or undefined behavior such as signed overflow or an out-of-bounds index — becomes a compile error, caught before any code runs.
constexpr std::array prices{10.0, 20.0, 30.0};static_assert(total_after_discount(prices) == 54);Calculations are more easily reusable.
Example
Section titled “Example”Read a comma-separated list of prices from stdin, print each price as it is processed, then print the total after a 10% discount.
Tangled
Section titled “Tangled”#include <cstdlib>#include <iostream>#include <string>
int main() { std::string line; std::getline(std::cin, line); // action: read
// Action and calculation tangled: parse, report, sum, discount and print in one body. double sum = 0.0; std::size_t start = 0; while (start <= line.size()) { auto end = line.find(',', start); if (end == std::string::npos) end = line.size(); if (end > start) { double price = std::strtod(line.substr(start, end - start).c_str(), nullptr); std::cout << "price: " << price << '\n'; // action in the middle of the loop sum += price; } start = end + 1; } double total = sum * 0.9; // 10% discount, buried right here
std::cout << "Total: " << total << '\n'; // action: write}The parse, the discount rule, the per-price output and the final output all live in main. To test the discount you must feed stdin and read stdout. To find out whether parsing handles a trailing comma, you must run the whole program.
Separated
Section titled “Separated”#include <cstdlib>#include <format>#include <iostream>#include <span>#include <string>#include <string_view>#include <vector>
// Calculation: pure. Same prices -> same report lines.std::vector<std::string> report(std::span<const double> prices) { std::vector<std::string> lines; for (double p : prices) lines.push_back(std::format("price: {}", p)); return lines;}
// Calculation: pure. Same prices -> same totaldouble total_after_discount(std::span<const double> prices) { double sum = 0.0; for (double p : prices) sum += p; return sum * 0.9; // 10% discount, one place}
// Calculation: pure. Same input string -> same prices.std::vector<double> parse_prices(std::string_view line) { std::vector<double> prices; std::size_t start = 0; while (start <= line.size()) { auto end = line.find(',', start); if (end == std::string_view::npos) end = line.size(); if (end > start) prices.push_back( std::strtod(std::string(line.substr(start, end - start)).c_str(), nullptr)); start = end + 1; } return prices;}
// Action: knows about stdin and stdout.int main() { std::string line; std::getline(std::cin, line); // action: read
auto prices = parse_prices(line); // calculation auto lines = report(prices); // calculation double total = total_after_discount(prices); // calculation
for (auto& l : lines) std::cout << l << '\n'; // action: write std::cout << "Total: " << total << '\n'; // action: write}All the output now happens at the end, after every calculation is done. The loop that prints is not mixed into the loop that computes, so the report is just data that can be asserted on.
main is now the only action; everything else is pure and directly testable without mocking, no setup, just values in and a value out:
assert(total_after_discount(std::array{10.0, 20.0, 30.0}) == 54);assert((parse_prices("1.5,,2") == std::vector<double>{1.5, 2}));