All programming paradigms are, essentially, illusions directed at helping our minds comprehend our programs’ state space. We all know that down there, in its raw form, code becomes a sequence of binary machine instructions that only care about registers and arithmetic. But what happens before that is magic!
Advancements in programming language development have significantly impacted our software, and through it, the world we live in. The natural question we can ask is: “How to make a good programming language?” Unfortunately, the answer isn’t simple.
There are thousands of programming languages out there. Of course, most of them either forgotten or never saw the light of day in the first place. The successful ones, usually have a purpose (or a few) ― be it server-side programming, systems design, machine learning, or something completely different. It would be ignorant to suggest that Python is better than Go because machine learning (done primarily in Python) is cooler than microservices (mostly made with Go).
Nevertheless, all programming languages can be measured by their ability to provide developers with a framework for reasoning about their program simply and efficiently. This benchmark is powerful enough to force programmers to switch from Assembly to C, from C to C++, from C++ to Java and so forth. One of the main tasks of a programming language is to ease the cognitive load. 99% of the time, languages that take over, do that better than their predecessors.
The Big Catch
“Programs must be written for people to read, and only incidentally for machines to execute.”
― Harold Abelson
There is one paradigm which is exceptional at making devs’ life easy: Pure Functional Programming. “Here is a function, it takes X and returns Y.” Pure functions don’t need to worry about internal state changes, mutations, and concurrency. Yet, there is one inconsistency that weakens cohesion in such languages: unavoidable side effects. If only we could detach our Haskell programs from the OS, we would achieve pure computation! But we can’t…
Far too often we fall for the “Everything is …” trap. Everything is a procedure/structure/function/object ― pick your favourite. This chase after the ultimate abstraction leaves us frustrated and confused.
“How can I make this pineapple look like a banana? They’re both yellow inside… surely, there has to be a way to coerce them together!”
Let us not chase the phantom and set our record straight: there are three concepts we work with.
- Data transformers
- Data transmitters
Data is information stored in memory that is the input and output of every computation. It can be moved around, transformed, destroyed, or ignored depending on our goals.
Data is seldom given to us in its desired form. An array is usually unsorted, shortest path yet to be found, “hello world” yet to be printed. We use functions to transform and shape the given data.
Most programming languages try to convince you that
getline are functions. “
printf receives your data as arguments and returns a
void, ― they say ― Isn’t it a function?” No, it isn’t!
Look at the definition above; functions transform data. It means that they accept data and return data. The function is your loving girlfriend: if you give her a present, you’ll surely get something in return.
printf is definitely not a function.
getline then? It takes no arguments, performs some trickery and gives you a string back. What is this? This sounds like a variable to me, yet, there is one caveat. Variables do not change unless mutated, but
getline always returns something new…
getline can be thought of as actions on the
stdio channel. The creators of C made them look like functions, but the smell is still there. The differences in their behaviour can be easily explained by their nature.
printf is directed outwards while
getline ― inwards. That’s it. This also explains why they don’t transform any data ― they are not supposed to!
The primary mission of a channel is data transmission.
The Main Principle of Monarchy
Now that the terminology is established, we can move on to th…
“You are a liar, Viktor! A BIG LIAR! If
printfwas actually a channel action, we’d be able to see the channels themselves, but there’s no such thing in any of the programming languages!”
― Reader (dear to my heart)
The answer here is simple. You don’t see channels because every programming language permits you to access them unconditionally and from any odd piece of code. In this sense, the channels are omnipresent, they are everywhere in your code, like ether; every time you call
printf, you can almost feel their divine presence.
Coincidentally, channels grant you the power to change the world. Pure computation is as tangible as a ghost in a sealed bunker, and quite frankly, not too useful. Having discovered this powerful tool, it would be foolish of us to allow every function to have access to it… right? You’d think so, but in the overwhelming majority of programming languages, any function can print to the console, create files, and listen on port 8080 if it really wants to. Even in Haskell ― just stick the IO monad on top, and you’re golden!
However, the IO monad is an afterthought. If you get what it really implies, it is quite disrespectful, actually.
― Yeah, man, you know, I decided I am going to perform a side-effect today.
― And who said you can do that?
― Didn’t you see my cool hat? It says IO! Stands for “I own you”.
In all hierarchies, permission flow has a top-down direction. Such explicitness puts peasants in check and makes them follow the rules. If you want to be a successful King, follow the main principle of Monarchy:
Alas, a Kingdom of many Kings is doomed.
It should now be clear that channels are synonymous with permissions. Pure functions have none ― they are confined to the data passed in. However, not all functions are pure. The
main function, for example, has to assume the role of a filthy omnipotent tyrant. How? Using the
OS channel is an all-mighty channel that grants you access to all available side-effects supported by your operating system. Standard input and output, sockets, web requests, file system, threading, and a bunch of other things. There is one more nice thing about it though ― it can be subdivided into weaker, more specialised channels.
OS to be divisible because you want to be precise with your permissions.
printf only needs the
stdout channel and the message ― nothing else. This atomisation of responsibility gives you precise control over your modules and increases code readability. Now, your functions are all very well-mannered.
― Dear Sir, may I have the privilege of talking to Ms FileSystem today? I am so curious to see what she’s hiding in her folders…
― Why, yes, of course! Here is your pass.
Sadly, Permissions-Driven in modern programming languages can only be achieved through tricks and magic (see this demo). There are similar concepts in a variety of languages (e.g. you can pass
Writer interface objects to functions in Go) but they are not enforced. You can still perform any side-effects you want from anywhere you like. You can even ignore the
Writer passed to you, create another file and write to it.
Hence, I am going to start working on an open-source permissions-driven functional programming language called Chan in this GitHub repo. All contributions ― be it feedback, ideas, syntax drafts or feature proposals ― are extremely welcome.
I hope that this article inspired you to dig deeper and search for meaning. Have a great day!