Think in Math, Code for Implementation
Explore why mathematics is a superior thinking tool for problem-solving and design in software development, contrasting its flexibility with programming languages as mere implementation tools.
Programmers frequently engage in discussions about programming languages. We not only debate their technical merits and aesthetic qualities, but these languages often become integrated into our personal identities, along with the values and traits we associate with them. Some even advocate for a form of linguistic determinism, suggesting that thinking is confined to what a language makes typable.
Given the significant amount of time we spend writing code, a keen interest in language design is certainly justified. However, the nature of these discussions often suggests we perceive languages as much more, perhaps overlooking their primary function. Programming languages are fundamentally implementation tools for instructing machines, not thinking tools for expressing ideas. They are strict formal systems, often riddled with design compromises and practical limitations. Ultimately, our hope is that they make controlling computers bearable for humans. In contrast, thoughts are best expressed through a medium that is free and flexible.
Thinking in Math
Mathematics, a natural language that has been effectively used for thinking about computation for thousands of years, offers this freedom. Most people don't perceive math as free or flexible; they associate it with intimidating symbols and rote memorization for tests. Others hear "math" and think of category theory, lambda calculus, or other highly formal methods for defining computation itself – but these are hardly necessary for programming itself.
I hope readers of this article have had a more positive experience with mathematics, perhaps through a course in graph theory, algorithms, or linear algebra. These are the kinds of courses that involve logic and theorems, written in prose mixed with symbols (most mathematical symbols, in fact, weren't even invented until the 16th century). This approach to mathematics focuses on creating logical models to understand real-world problems through careful definitions and deductions. If you seek a clearer understanding of this, I recommend works by Trudeau, Stepanov, or Manber.
Math enables you to reason about logical structures, free from other constraints. This is precisely what programming requires: creating logical systems to solve problems. Consider the basic pattern for programming:
- Identify a problem
- Design algorithms and data structures to solve it
- Implement and test them
In practice, this work isn't always so neatly organized, as there's often interplay between these steps. You might write code that informs the design. Even so, this basic pattern is followed repeatedly.
Notice that steps 1 and 2 consume most of our time, ability, and effort. Crucially, these steps do not naturally lend themselves to programming languages. This doesn't stop programmers from attempting to solve them directly in their editor, but the result is often muddled, slow, or solves the wrong problem entirely. It's not that programming languages aren't advanced enough; it's that no formal language could excel at these initial stages. Our brains simply don't think that way. When problems become complex, we draw diagrams and discuss them with collaborators.
Ideally, steps 1 and 2 are fully resolved first, and only then is a programming language used for step 3. This approach has an added benefit of transforming the implementation process. With a mathematical solution in hand, you can then focus on choosing the best representation and implementation, writing better code because you clearly understand the end goal.
Implementation Concerns
Why do programming languages become burdensome as thinking tools? One reason is their inseparable connection with implementation concerns. A computer is a device that must manage various tasks while constrained by physical and economic limitations.
Consider all the factors when writing a simple function:
- What inputs should it receive?
- How should they be named?
- What types should they be? (Even dynamically typed languages implicitly consider types.)
- Should they be passed by value or by reference?
- Which file should the function reside in?
- Should the result be cached for reuse, or is recalculation fast enough each time?
This list goes on. The crucial point is that these considerations are entirely unrelated to what the function does; they distract from the problem the function aims to solve. Many languages attempt to hide such details, which is helpful, especially for mundane tasks. However, they cannot transcend their role as mere implementation tools. SQL, one of the most successful examples, is still ultimately concerned with implementation details like tables, rows, indices, and types. Consequently, programmers still design complex queries in informal terms, often thinking about what they want to "get" before writing numerous JOINs.
Inflexible Abstractions
Another limitation of programming languages is their inadequacy as abstraction tools. In engineering, "abstraction" typically means hiding implementation details. A complex operation or process is packaged into a "black box" with its contents hidden, exposing only well-defined inputs and outputs. This box is accompanied by a simplified explanation of its function.

Black boxes are essential for engineering large systems because the underlying details are too overwhelming to manage mentally. However, they also have well-known limitations. A black box leaks because its brief description cannot fully determine its behavior. Opaque interfaces introduce inefficiencies, such as duplication and fragmented design. Most importantly for problem-solving, black boxes are rigid. They must explicitly reveal some "dials and knobs" while hiding others, committing to a particular view of what is essential for the user and what is considered noise. In doing so, they present a fixed level of abstraction which may be too high-level or too low-level for the specific problem. For example, a high-level web server might provide an excellent interface for serving JSON but be useless if one needs an interface for serving incomplete data streams, such as output from a running program. In theory, you can always look inside the box, but in code, the abstraction level at any given time is fixed.
In contrast, the word "abstraction" in mathematics is not about hiding information. Here, abstraction means extracting the essential features or characteristics of something, relative to a particular context. Unlike black boxes, no information is hidden; mathematical abstractions don’t "leak" in the same way. You are encouraged to adjust to the most appropriate level of abstraction and quickly shift between different perspectives. You might ask:
- Is this problem best represented as a table? Or, a function?
- Can I view the entire system as a function?
- Can I treat this collection of items as a single unit?
- Should I examine the whole system or just a single part?
- What assumptions should I make? Should I strengthen or weaken them?
Consider the many ways to view a function:

Thinking mathematically allows you to use whichever perspective brings the most clarity at any moment. It turns out that most abstract concepts, just like functions, can be understood from multiple viewpoints. Studying mathematics provides a versatile toolbox of perspectives for analyzing all kinds of problems. You might first describe a problem with a formula, then switch to understanding it geometrically, then recognize that some group theory (abstract algebra) is relevant, and all of this combines to provide deeper insight and understanding.
To summarize, programming languages are excellent engineering tools for assembling black boxes; they provide functions, classes, and modules, all of which help encapsulate code into well-defined interfaces. However, when you're trying to solve problems and design solutions, what you truly need is the mathematical kind of abstraction. If you attempt to think directly at the keyboard, the available black boxes will inevitably warp your perspective.
Problem Representation
Just as programming languages are rigid in their ability to abstract, they are also rigid in how they represent data. The very act of implementing an algorithm or data structure involves picking just one of many possible ways to represent something, along with all the inherent trade-offs. It's always easier to make these trade-offs when you have specific use cases in mind and a clear understanding of the problem.
For example, graphs (sets of vertices and edges) appear in many programming problems, such as internet networks, pathfinding, and social networks. Despite their simple definition, choosing how to represent them is challenging and varies greatly depending on the use case:

The representation that most closely matches the mathematical definition might be:
vertices: vector<NodeData>
edges: vector<pair<Int, Int>>
(The vertices can be removed if you only care about connectivity.)
If you need to traverse a node’s neighbors quickly, you would probably want a node structure like:
Node { id: Int, neighbors: vector<Node*> }
You could also use an adjacency matrix, where each row stores the neighbors of a particular node:
connectivity: vector<vector<int>>
and the nodes themselves are implicit.
Pathfinding algorithms often work on graphs implicitly from a board of cells:
walls: vector<vector<bool>>.
In a peer-to-peer network, each computer is a vertex and each socket is an edge. The entire graph isn’t even accessible from one machine!
Math allows you to reason about the graph itself, solve the problem, and then choose an appropriate representation. If you think solely in a programming language, you cannot delay this decision, as your first line of code commits you to a particular representation.
Note that graph representations are too diverse to be encapsulated in a polymorphic interface. (Consider again a graph representing a computer network, like the entire internet.) Therefore, creating a completely reusable library for "graphs" is impractical. Such a library could only work on a few specific types or force all graphs into an inappropriate representation. This doesn't mean libraries or interfaces aren't useful; similar representations are needed repeatedly (like std::vector), but you cannot write a library that encapsulates the concept of "graph" once and for all. A simple generic interface with a few types in mind is more appropriate.
As a corollary, programming languages should focus primarily on being useful implementation tools, rather than theoretical ones. A good example of a modern language feature that does this is async/await. It doesn't hide away complex details or introduce new conceptual theory; it takes a common practical problem and makes it easier to write.
Thinking in math also makes the "C style" of programming more appealing. When you understand a problem well, you don't have to build layers of framework and abstraction in anticipation of "what if" scenarios. You can write a program tailor-made to the problem, with carefully chosen trade-offs.
Example Project
So, what does thinking in math look like in practice? For this section, you may need to read a bit more slowly and carefully.
I recently worked on an API at work for pricing cryptocurrency for merchants. It considers recent price changes and recommends that merchants charge a higher price during volatile times. Although we conducted theoretical research, we wanted to empirically test its performance under various market conditions. To do so, I designed a bot to simulate a merchant conducting business with our API and observe its behavior.

Preliminaries
- Definition: Exchange Rate
r(t)is the market rate offiat/crypto. - Definition: Merchant Rate
r'(t)is the modified exchange rate the merchant is advised to charge customers. - Definition: Purchase When a customer buys an item, we call that event a purchase. A purchase consists of the price in fiat and a time:
p = (f, t). - Theorem: The amount of crypto for a purchase is found by applying the modified exchange rate:
t(p) = p(1) / r'(p(2)).- Proof:
p(1) / r'(p(2)) = fiat / (fiat/crypto) = fiat * crypto/fiat = crypto.
- Proof:
- Definition: Sale When the merchant sells their crypto holdings, we call that event a sale. A sale consists of an amount in crypto and a timestamp:
s = (c, t). - Theorem: The amount of fiat the merchant obtained from a sale is found by applying the exchange rate to the sale:
g(s) = s(1) * r(s(2)).- Proof:
s(1) * r(s(2)) = crypto * (fiat/crypto) = fiat.
- Proof:
- Definition: Balance The balance of a set of purchases
Pand salesSis the difference between all purchase crypto amounts and all sale crypto amounts.b(P, S) = sum from i to N of t(p_i) - sum from j to M of s_j(1)- Note that
b(P, S) >= 0must always hold.
- Definition: Earnings The earnings of a set of purchases
Pand salesSis the difference between sale fiat amounts and purchase fiat amounts.e(P, S) = sum from j to M of g(s_j(1)) - sum from i to N of p_i(1) >= 0.
Objective
- Definition: We say that the merchant rate is favorable if and only if the earnings are non-negative for most sets of typical purchases and sales.
r'(t)is favorable iffe(P, S) >= 0.- In a favorable case, the merchant did not lose any fiat by accepting crypto.
- The terms most and typical will not be rigorously defined here.
- As part of typical, we can assume that merchants will sell their crypto in a timely manner. So, assume
s_i(2) - s_j(2) < Wfori,j in {1.. M}for some boundW. - Purchase amounts should be randomly distributed within a reasonable range for commerce, perhaps $10-100.
- The goal of the bot is to verify that
r'(t)is favorable. - Note that this definition is only one measure of quality. Perhaps protecting against the worst case is more important than simply being favorable. In that scenario, we would be concerned about the ability to construct a set of purchases with very negative earnings.
Algorithm
Repeat many times:
- Randomly choose a time range
[t0, t1]. - Generate a set of purchases at random times within
[t0, t1]. The price should fall within a range[p0, p1]of typical prices. - Generate a set of sales at evenly spaced times (perhaps with slight random noise) within
[t0, t1]. Each sale should be for the fullbalanceat that time. - Calculate the
earningsfor these sets. - Record the earnings.
After the repetitions:
- Report how many earnings were negative and non-negative. Show a percentage for each.
- Identify and report the minimum and maximum earnings.
Conclusion
As you read this example, you might find its statements obvious. Certainly, none of these steps are inherently difficult. However, it was surprising how many of my initial assumptions were corrected and how challenging it was to choose an objective definition for a favorable outcome. This process helped me become aware of assumptions I would not have even considered if I had simply started by writing code. Perhaps the greatest benefit was that after defining it mathematically, I could quickly review it with a co-worker and make corrections that were easy on paper but would have been difficult to change in code.
I hope that thinking in the language of mathematics will bring similar benefits to your projects! Note that this example represents just one style of utilizing mathematical thinking.