Swift Blog Carnival
Inspired by https://christiantietze.de/posts/2026/04/swift-blog-carnival-tiny-languages/
Setup
I love maths. I know it's not a popular topic in school, and comes with a lot of baggage for most people, but for me, looking at numbers and figuring out how they relate to each other or what they can tell me about the thing they measure is very engaging.
Unfortunately, maths is also something either very clunky or absent in most general purpose languages. As much as I appreciate Swift's largely successful attempt at preventing coding errors through various tricks like strong type and nil checking, concurrency enforcement, etc, using it for maths problems is somewhat difficult.
For reasons that are lost to the mists of time, I started working on a symbolic math representation in Swift.
What Is Symbolic Math And Why Do You Torture Yourself So?
When "doing maths", we tend to treat symbols (∂, ¬, x), but also numbers, as things that have no inherent value until you need it to. That's where you might have heard the expression "just plug in the numbers in the formula". "Alice's age is twice the age of Bob" has meaning, even when you don't know the precise ages of Alice and Bob. Of course, "twice" means "multiplied by two", but because we don't have the rest of the numbers yet, we treat 2 as another symbol, in this context. It will take the value 2 and allow me to calculate things if and when I get Alice's or Bob's age.
Symbolic math is basically your Xs and Ys, and working with them even though they don't have a definite value.
They have a type though, and meaning, and other constraints that make working with those variables possible.
So far so good, it's exactly like the variables in your programs: they have a type, they can hold any value within that type, and you work with them until somewhere down the line something is done with them.
OK, so now, imagine you have an array of Int called versions.
- What does
versions - 1mean? - What does
versions > 1mean?
In languages like Julia, you can broadcast a function to every single element in a collection, and because of various safeguards, it can even be done in parallel. In math-y languages some of the above statements actually have meaning, and automagically broadcast or filter the array.
This category of wants can be solved by defining operators and playing with the type system in Swift. We just have to agree on what it means:
extension Array where Element : SignedInteger {
static func + (left: Array<Element>, right: Element) -> Array<Element> {
if left.isEmpty { return [] }
return left.map { $0 + right }
}
static func - (left: Array<Element>, right: Element) -> Array<Element> {
if left.isEmpty { return [] }
return left.map { $0 - right }
}
}
let a = [0,1,2,3,4,5,6]
let b = a + 1
let c = a - 1
print(a)
print(b)
print(c)
yields
[0, 1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6, 7]
[-1, 0, 1, 2, 3, 4, 5]
I Sense There Is a "But" Coming...
But Swift is not built to manipulate unknowns. How do you define a variable that is definitely an Int but has no value yet?
Most people will use Optionals to capture that: what is an Int? but an Int that doesn't have a value yet?
The semantics of Int? + 2 by default are clear though: it is forbidden, you have to provide a default value in case of nil, and if you define the operator + to be able to handle the nil case, you kind of defeat the purpose of nil checking and the semantics of the operation are still murky.
Ultimately, that's because in maths we can do something like
- y = x + 2
- ...
- x = 1
and somehow, magically, y now evaluates to 3, whereas at the start, it didn't have any value, especially not nil.
The pointer-minded people among you might want to solve this kind of problem with classes or pointers and therefore have a y variable that can retroactively change values, and that works (kind of )! But it means going through hoops, and defining classes that have optionals in them, handle errors, etc...
What IS The Problem You Are Trying To Solve?
Because I was handling a lot of tabular data at the time, I wanted to be able to say "for every row, the column D contains the sum of the columns A and B, multiplied by C", without doing the actual calculation unless I needed it, and preferably defined once, for the whole column, instead of the copy-paste rigamarole of Excel-like software with their very dangerous reference system.
| A | B | C | D |
|---|---|---|---|
| 1 | 2 | 3 | ? |
| ... | ... | ... | ? |
If I could somehow define D = (A + B) * C, instead of [D1 = (A1 + B1) * C1, D2 = (A2 + B2) * C2, ...], and have D contextually resolve to the right value when I need it, it would somehow feel... more elegant?
DSL to the Rescue!
Here's a unit test from the project:
let expr : SYMExpression = (1 + 2 * "X") * (3 - "Y")
let values = ["X":1, "Y":2]
print("\(expr.asString) => \(try expr.evaluate(&values))")
XCTAssert(try expr.evaluate(&values) == 3)
And it works!
The syntactic idiosyncrasies of having the symbolic data as Strings is kind of iffy, but when you think about it, symbols are names, and I couldn't find another way to represent variables anyways. Plus they can be emojis or whatever. So I guess that's a bonus.
The eagle-eyed among you may have noticed I pass the values as inout. That's because the symbolic thing also handles systems:
let test = SYMSystem {
try (1 + 2 * "X") > (3 - "Y")
try "X" != "Y"
}
With the idea of passing them a Domain (from my repo HoledRange), and having the system give me back the applicable Domain for these variables.
One definite possible application is letting me know if there are no values that would satisfy this system, so that I don't bother calculating anything, and another one is identifying is there is like a single possible value for all the variables, which simplifies the calculus immensely as well.
Anyways, the project didn't go further than being satisfied by watching unit tests go green, and my probably unhealthy affection for maths, but the code is surprisingly small: without the Domain calculations, everything fits in 300 lines of code, most of which is extensions for normal types like Double and Int to conform to custom protocols, and operator overloads to handle this new kind of type.
But, even though I don't actually use it in production, how cool is that to be able to define a whole category of types and their syntactic sugar, just for the kick of seeing (1 + 2 * "X") * (3 - "Y") be a valid expression that has no current value, but can, at any moment, resolve to an actual usable number? And the equation system thing has potential, if I ever find the time and motivation to make it work fully, outside of toy cases.