Convivial Code: The Praxis of Structure (Part 1)
Introduction
I have a thesis: that “Software Engineering”, to be a rigorous discipline, must be praxis: the embodiment of abstract theory in action.
Without praxis “Software Engineering” is merely “coding”. A task of imperatively and operationally telling the machine, line by line, what it must do to produce a result.
When we rush to define the operations in code without first defining and specifying the domain of the problem and the goal of the program…we make a mess of things. Severing the link between theory and execution makes implementation and maintenance harder, especially in the long run. Not all problems require this degree of formality but because human brains are free energy minimizers, we almost always underestimate when we should exercise this tooling.
The result, IMHO, is code that destroys (or makes a mess of) the structure of the problem it solves, flattening the topology of the domain. If the structure was ever even understood it then only resides within the point-in-time practitioner’s mind.
To avoid this gumption trap, I think we must view code not as a private command script, but as a public artifact. A simple directive can help orient us towards this: “show your thinking”. I believe we must cultivate a civic empathy for the people who will maintain this code when we have moved on (this is also often our future selves).
While there are good intellectual arguments for showing your thinking, IMHO, the civic argument is stronger. It connects us to our peers, to the “commons” of the codebase. In this light, “showing your thinking” is not just good documentation, it is an expression of convivialism, in the vein of Ivan Illich’s Convivial Tools.
Convivial software strives for denotative properties that respect the maintainer; and, even the user:
- transparency
- to empower the maintainer to reason about its nature without needing to reify the operational semantics of the program within one’s head (often through trial-and-error “run and find out” or biology experiments).
- compositionality
- to provide well-defined interfaces so that parts can be combined without deep knowledge of their internals. This parallels the civic ideal of trust: I can trust the component to hold its contract without needing to police its internal state.
- containment
- to tell the truth about the messy things the program must do. Side-effects, mutations, and hidden state are the “secrets” of a program; convivial code exposes them.
These principles can feel slippery in the (sometimes) less precise world of software. As Gabriella Gonzalez once advised me: learning math will help you think more precisely. To really understand the necessity of structure, I think we need to step into a domain where structure is unforgiving: math.
My math tutor taught me how to formalize word problems and is also credited with the memorable advice, that I make use of often outside of math, to “be careful when multiplying out because you almost never want to do it, you almost always want to retain the factored form of an expression”.
So…showing your thinking isn’t merely a pedagogical hoop but a crucial way of preserving the topology of a problem against the entropy of calculation.
Before applying this praxis to code, I want to first show how it saves us in the realm of raw quantity.
NB: there is an important connection between math and programming languages via the Curry-Howard correspondence.
An Example in (Beginner) Math
Let’s get started, feeling this out, with a word problem from beginner linear algebra that my math tutor gave me last year
The problem
(I don’t know where the original problem came from but I asked Gemini to reconstruct it as this is a common construction resource allocation problem.)
A civil engineering firm requires exactly 480 tons of sand, 580 tons of fine gravel, and 570 tons of coarse gravel to create a specific road base mixture for a new highway project. The firm can hire three independent suppliers who deliver pre-mixed loads from different quarries: Supplier A’s trucks carry a mix of 6 tons of sand, 3 tons of fine gravel, and 1 ton of coarse gravel at a cost of $200 per load; Supplier B’s trucks carry 2 tons of sand, 5 tons of fine gravel, and 3 tons of coarse gravel at a cost of $250 per load; and Supplier C’s trucks carry 4 tons of sand, 2 tons of fine gravel, and 5 tons of coarse gravel at a cost of $300 per load. Determine the exact number of loads required from each supplier to satisfy the material specifications perfectly and calculate the total cost of this optimal transport schedule.
… we can’t jump into solving this problem by haphazardly throwing math at this because we have independent variables (suppliers), competing constraints (sand, fine gravel, coarse gravel), and an objective function (cost).
It’s time formalize and use our tools.
Formalize
We have three suppliers with three material mixes:
- Supplier A carrying 6 tons of sand, 3 tons of fine gravel, and 1 tons of coarse gravel at a cost of $200 per-load.
- Supplier B carrying 2 tons of sand, 5 tons of fine gravel, and 3 tons of coarse gravel at a cost of $250 per-load.
- Supplier C carrying 4 tons of sand, 2 tons of fine gravel, and 5 tons of coarse gravel at a cost of $300 per-load.
We have three constraints:
- 480 tons of sand,
- 580 tons of fine gravel; and,
- 570 tons of coarse gravel.
We must determine the exact number of loads required from each supplier and calculate the total cost of the optimal transport schedule.
First we declare our variables (there are four):
\[ \begin{align} x &= \text{number of supplier A loads} \nonumber \\ y &= \text{number of supplier B loads} \nonumber \\ z &= \text{number of supplier C loads} \nonumber \\ t &= \text{total cost} \end{align} \]
Then, we need to write down the four equations we can find in the problem statement:
\[ \begin{align} s_{1}x + s_{2}y + s_{3}z &= 480 \nonumber \\ f_{1}x + f_{2}y + f_{3}z &= 580 \nonumber \\ g_{1}x + g_{2}y + g_{3}z &= 570 \nonumber \\ c_{1}x + c_{2}y + c_{3}z &= t \end{align} \]
… which, substituting what we know of the material mixes and costs for each supplier, turns into:
\[ \begin{align} 6x + 2y + 4z &= 480 \nonumber \\ 3x + 5y + 2z &= 580 \nonumber \\ x + 3y + 5z &= 570 \nonumber \\ 200x + 250y + 300z & = t \end{align} \]
We are ultimately solving for \(t\).
Solve
Now that we have modeled the problem with variables and equations, we can now work on solving it and many tools open up to us, for example we can translate the system of three equations into a matrix and use Gaussian elimination to reduce it (you can also solve the system of three equations using the substitution or elimination methods as well):
\[ \begin{bmatrix} 6 & 2 & 4 \\ 3 & 5 & 2 \\ 1 & 3 & 5 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \end{bmatrix} = \begin{bmatrix} 480 \\ 580 \\ 570 \end{bmatrix} \]
… using Gaussian elimination and an augmented matrix, we can solve for our three variables \(x\), \(y\), and \(z\):
\[ \begin{align*} &\text{1. The Augmented Matrix Setup} \\ \hline \\ &\left[ \begin{array}{ccc|c} 6 & 2 & 4 & 480 \\ 3 & 5 & 2 & 580 \\ 1 & 3 & 5 & 570 \end{array} \right] \\[1em] &\quad \downarrow \text{ ... solving via Gaussian Elimination ...} \\[1em] &\text{2. The Result in Row Echelon Form} \\ \hline \\ &\left[ \begin{array}{ccc|c} 1 & 3 & 5 & 570 \\ 0 & -4 & -13 & -1130 \\ 0 & 0 & 26 & 1580 \end{array} \right] \\[2em] &\text{3. Back Substitution} \\ \hline \\ &\text{Step A: Solve for } z \text{ (from } R_3 \text{)} \\ &\qquad \begin{aligned} 26z &= 1580 \\ z &= \tfrac{790}{13} \approx 60.77 \text{ loads} \\ \end{aligned} \\ \\[1em] &\text{Step B: Solve for } y \text{ (substitute } z \text{ into } R_2 \text{)} \\ &\qquad \begin{aligned} -4y - 13(\tfrac{790}{13}) &= -1130 \\ -4y - 790 &= -1130 \\ -4y &= -340 \\ y &= 85 \text{ loads} \\ \end{aligned} \\ \\[1em] &\text{Step C: Solve for } x \text{ (substitute } y, z \text{ into } R_1 \text{)} \\ &\qquad \begin{aligned} x + 3(85) + 5(\tfrac{790}{13}) &= 570 \\ x + 255 + \tfrac{3950}{13} &= 570 \\ x + \tfrac{7265}{13} &= 570 \\ x &= \tfrac{145}{13} \approx 11.15 \text{ loads} \end{aligned} \\ \end{align*} \]
… now that we have values for all three of our variables, we can find the cost, but note that mathematically the solution requires partial trucks (11.15 and 60.77). In the physical domain, we cannot dispatch partial trucks, therefore to meet the minimum constraints, we must apply a ceiling (\(\lceil\rceil\)) function:
\[ \begin{align*} &\qquad \begin{aligned} x &= \lceil 11.15 \rceil = 12 \\ y &= \lceil 85.00 \rceil = 85 \\ z &= \lceil 60.77 \rceil = 61 \end{aligned} \\ \\[1em] &\text{Recall our fourth equation:} \\ &\qquad t = 200x + 250y + 300z \\ \\[1em] &\text{Substitute in } x, y, \text{ and } z: \\ &\qquad \begin{aligned} t &= 200(12) + 250(85) + 300(61) \\ &= 2400 + 21250 + 18300 \\ &= 41950 \end{aligned} \end{align*} \]
Therefore, the total cost for the optimal schedule that meets the minimum constraints is $41,950.00 with the optimal schedule being:
- 12 trips for Supplier A,
- 85 trips for Supplier B; and,
- 61 trips for Supplier C
An Example in Software Engineering
… to be continued in a second part.