Shape & Flow.
Towards an even more cleaner code.
Hyperlatinate software.
The software world is hyperlatinate. That's hyperlatinate for "uses too much fucking Latin."
Ok, hyper is actually from Greek, but that's besides the point. Actually, fine, it's the same point. The software world is hypergrecolatinate. Don't you like that better?
The main reason for this is that people speak in registers. The register that naturally formed around tech came about from groups of highly schooled individuals who had already been thoroughly soaked in the linguistic pools of their universities, where they got a chance to swim with biologists, doctors, and lawyers—all of whom speak this Latin-Greek gibberish fluently.
“A monad is just a monoid in the category of endofunctors. Duh.”
— A highly schooled individual on why Haskell is EZ.
For the rest of us, trying to understand what the hell anybody is talking about ever becomes a game of scrambling through thesauruses until we finally land on the variation of the word that's in English.
The fact is, the casual register of English that most people speak in favors simple words with Germanic roots, and mostly eschews (uh oh, getting French...) words with Latin and Greek roots.
I'm not here to knock on either Latin or Greek. I think they are beautiful languages, on their own. My only gripe is that the way they are dropped into English today creates a separate register of the language that effectively hides a lot of knowledge from the average person.
Complexity is the problem, as it always has been. And if a person has to translate a word before they can understand it, that adds complexity. Thus, I would rather use the Latin-derived version of a word if it's easier to grok. And, to be fair, how would you say 'endofunctor' without making up a new fancy name for it?
But, for all the noise I'm making about language, we have the word software. I like that word. It has just two simple parts: soft + ware. I don't really know what it means without context, but I know that somebody is peddling some wares, and I know that they are soft.
What if all software words were like this?
Some mo' bettah words.
Let's get into how I think things should be. I might be completely wrong about everything I say from now on, but that's alright, cause it's just like my opinion, man.
Variables & Constants.
You might have already learned what variables are in grade school algebra class, but even then, you probably had to have someone explain it to you:
“Well, um, a variable is like a box that can hold any number...”
— Your grade school algebra teacher, wondering if he picked the right job.
The question that naturally follows is, "so why not just call it a box?"
// Create a box named a
new box a
// Put 4 into box a
a = 4
// Now put 5 into box a instead
a = 5
A constant is also a type of box, but its number can't change. You can imagine that the box is locked with the number inside. You can only look at it. So let's call that a locked box.
// Create a box named b, put the number 4 in it, then lock it and throw away the key!
new locked box b = 4
// Wrong: box b is locked and its contents can't be changed
b = 5
// You can even imagine creating a normal box, and then locking it after putting a number into it
new box c
c = 42
lock c
Of course, in programming, the boxes don't just hold numbers. They can hold many different values, or more simply, things.
new box greeting
// greeting holds a text instead of a number
greeting = "Hello, world!"
Words defined:
- Box: something which holds other things, a variable.
- Locked box: a box which holds the same thing forever, a constant.
- Thing: well, you know what a thing is, in this case it's a value.
Types, Structs, & Classes.
If boxes hold things, then the question we have to answer about any one thing is, "what kind of thing is it?". We usually call this a type in programming, which honestly means the same thing as 'kind', so whatever, either one is fine.
The typical primitive types, a.k.a. the types that the programming language already understands, they're OK. They're kinda weird: int, int64, float, bool, string, char etc., but it would be hard to replace them with anything else, because they all have specific meanings.
// Create an 'int box', a box that only holds integers.
new int box myInteger = 69420
// Wrong: Can't put a string into an int box.
myInteger = "Hello"
// Of course, most languages would drop 'box' at this point,
// since it's implied:
new int myAge = 34
// These boxes can also be locked:
new locked int foreverTwentyOne = 21;
We might also want to create our own names for types:
// Create a type CoolNumber which is the same as an int
new type CoolNumber is int
// You can put ints in a CoolNumber box just the same:
new CoolNumber whoaSuchACoolNumber = 6
But what the hell is a struct? We can guess that a person was trying to say 'structure' before getting cut off, but that still doesn't really tell us anything. Turns out that it's a bunch of boxes all stored in the same place under one name. So why don't we just call that a group?
// Create a group type named Customer:
new group Customer {
locked int id
string name
box otherData // Holds anything
}
// Now create a Customer group by putting a thing in each of its boxes:
new Customer myFavoriteCustomer = { 1, "Willy Wonka", false }
// Replace things inside the boxes in myFavoriteCustomer:
// Wrong: Locked after it gets the first thing!
myFavoriteCustomer.id = 2
// Replace a string with a string
myFavoriteCustomer.name = "William Wonkington"
// Replace a boolean with string. This is fine since otherData doesn't have a type.
myFavoriteCostumer.otherData = "A really cool guy"
A class sounds like something borrowed from taxonomy, and with its associated idea of inheritance, I'm guessing that it was. But what it really is, is a kind of box, which, much like a group, holds other boxes. However, a class also holds rules about what it can do. So for the sake of making things simple, I'm going to call a class a doer.
new doer Dog {
string furColor
bool hasBadBreath
// What does a dog do?
// ...
}
new Dog myDoggy = { furColor: "brown", hasBadBreath: true }
But what is it that a doer does? Let's talk about that.
Statements, Functions & Methods.
When I write:
new int myAge = 34
that is usually called a statement. All it really refers to is a single piece of meaningful work that the computer does, so that's what I'm going to call it. Think of it like a sentence. A word is a good start, but you need a whole sentence to say something meaningful.
You've also probably heard of a function. Here's what Wikipedia says a function is:
In computer programming, a function (also procedure, method, subroutine, routine, or subprogram) is a callable unit[1] of software logic that has a well-defined interface and behavior and can be invoked multiple times.
To me, a function serves the role of the paragraph. It could be one sentence, or it could be ten, but it's really one or more pieces of work arranged in a specific flow to achieve an end. And every flow takes zero or more things as inputs, and outputs zero or more things.
// The flow 'sayHello' takes no inputs,
// writes a general greeting,
// and outputs nothing
new flow sayHello() {
// Run the flow 'writeLine' with input "Hello!"
writeLine("Hello!")
}
// Run the flow like this:
sayHello()
// The flow 'greetStranger' takes a string called name as input,
// writes a greeting with a name,
// and outputs nothing
new flow greetStranger(string name) {
new string message = "Hello, " + name + "!"
writeLine(message)
}
greetStranger("Mr. President")
// Let's drop the 'new' keyword, because it's understood at this point that when
// we name a box or a type of box, we are creating a new one.
// The flow 'addTwoInts' takes two ints as input,
// and outputs the result of adding them as an int
flow addTwoInts(int a, int b) outputs int {
output a + b
}
int result = addTwoInts(5, 6)
// The flow 'addTwoIntsWithError' takes two ints as input,
// tries to compute a result
// and outputs the result as a possible int along with a possible error
flow addTwoIntsWithError(int a, int b) outputs (int?, error?) {
int result = a + b
if no result {
output none, new error "Didn't get a result"
}
output result, none
}
int result, error err = addTwoInts(5, 6)
So now, instead of 'calling a function', we are simply 'running a flow'. I'll be honest, I like that a lot.
What about those Doers that we talked about earlier? Well, we talked about how they can do things, and what they can do is run flows. A function scoped to a class is usually referred to as a method. In our new little world, it seems natural that doers perform tasks. That helps us understand that only the doer is responsible for running that flow:
doer Dog {
task bark() {
WriteLine("Woof!")
}
}
Dog myDoggy = new Dog()
// Writes "Woof!"
myDoggy.Bark()
The boxes inside of classes are usually referred to as public and private, to show if things outside of the doer can initiate them, but for our doers, I'm going a little bit more everyday, and calling them shared and hidden:
doer BankAccount {
shared locked string bankName
hidden locked string accountNumber
hidden float balance
shared task make(
string bankName,
string accountNumber,
float balance
) {
my::bankName = bankName
my::accountNumber = accountNumber
my::balance = balance
}
shared task getBankName() outputs string {
output my::bankName
}
shared task deposit(float amount) {
my::balance += amount
}
shared task withdraw(float amount) outputs bool {
if my::isAcceptedAmount(amount) {
my::balance -= amount
output true
}
output false
}
hidden task isAcceptedAmount(float amount) outputs bool {
output my::balance - 20 > amount
}
}
BankAccount personalAccount = new BankAccount(
"Scrooge's Coin Emporium",
"8675309",
1000000000000.25
)
if personalAccount.withdraw(100000000) {
writeLine("Wohoo!")
} else {
writeLine("D'oh!")
}
Notice that the doer "talks" in the first person, with the special keyword my::
, which makes sense if you think about
the doer as owning its own boxes and tasks. You'll often see self
or this
used instead, but I think my
makes things
just a little bit clearer. And it's a little nod to Perl ;)
There's also the make
task, which is a friendlier version of construct
or similar. It's just a task that defines what
happens when you make a new doer.
You've probably seen the word static
thrown around, and it's one which newcomers often are puzzled by. All it means is that you can run
a doer type's task without having an actual doer around, because it doesn't use any of the doer's personal boxes.
public static
means that the task can be called from anywhere, so I'm going to call that a common task.
private static
means that only doers of the same type of doer can call it, but it doesn't use any doer's boxes. It's like a
little utility just for the doers, so let's call those root tasks.
// Let's replace the 'outputs' keyword with a simple ':' for brevity
doer WeatherStation {
root locked string CELCIUS = "Celcius"
common task getCurrentTemperature(string unit): float {
float temp = root::fetchTemperatureFromAPI()
if unit == root::CELCIUS {
output root::convertFahrenheitToCelcius(temp)
} else {
output temp
}
}
root task fetchTemperatureFromAPI(): float {
// flow to fetch temperature...
}
root task convertFahrenheitToCelcius(float temp): float {
output (temp - 32) * 5 / 9
}
}
// Use a common task
float currentTemperature = WeatherStation::getCurrentTemperature("Celcius")
Interfaces.
Interfaces are a way to outline a doer's responsibilities. An interface lists which tasks a doer should have, but it doesn't say how it should actually do them. Therefore, I think the best way to describe an interface is as a shape. It's only the shape of the thing, not the thing itself. When a doer fulfills a shape's responsibilities, we can say that it fits the shape. This is especially fitting in a Ports & Adapters scheme (or Ports & Plugins, as I would call them). Let me show you what I mean:
// Any plugin that fits the shape of the DatabasePort
// can be used for flows that need it
shape DatabasePort {
shared task save(string data)
shared task fetch(string id): string
}
// So this one can be used:
doer MySQLPlugin fits DatabasePort {
shared task save(string data) {
// Save to MySQL
}
shared task fetch(string id): string {
// Fetch from MySQL
output "Fetched data"
}
}
// ...or this one:
doer MongoDBPlugin fits DatabasePort {
shared task save(string data) {
// Save to MongoDB
}
shared task fetch(string id): string {
// Fetch from MongoDB
output "Fetched document"
}
}
// This flow needs a DatabasePort
flow saveToDatabase(string data, DatabasePort db) {
db.save(data)
}
// We make a box called db for a MySQLPlugin
MySQLPlugin db = new MySQLPlugin(...)
// The data will now be saved to the MySQL database
saveToDatabase("My very important data", db)
Lists, Maps, & Stuff.
Nothing wild to report here. I prefer list over array, but they mean different things in different languages. Map is better than dictionary.
box[] myRandomList = [1, "two", 3.0, false]
int[] myIntList = [1, 2, 3, 5]
User[] myUserList = [{id: 1, name: "John"}, {id: 2, name: "Steve"}]
map<char:box> myMap = {
a: 22,
b: "banana",
c: true
}
At this point, I'm running out of steam as far as programming language words goes, but there is one more thing I wanted to talk about:
Domain Driven Development
I think Domain Driven Development is a great idea. It's probably the best way to write software. The basis of DDD is that you write your code starting with the core business logic first. Then you write all the rest of the code needed to make your system work around that core. Ideally, the core never even knows about the rest of your app.
I can't imagine a better way to make sure that your app fits the problem you're trying to solve. Again, my problem is with the language used. Actually, the whole reason why I wrote this article is because I saw a term written down:
Domain Entities
What the fuck? I'm just trying to make an app, and now I have to worry about ghosts in my realm? 👻
Hella spooky.
So yeah, let's fix that.