Going further in programming with simple mental concepts
2024.07.09 | Sven Köppel
I was late in the game. My Python moment was about 12 years ago when I realized the earnings of reduction. Python, that was suddenly no more brackets and semicolons but a strict "everything is an object" idiom with an expressive function argument notation. The large standard library, the well-usable collection of integrated data types makes the learning curve very flat. The language design is so reduced that it is easy to develop (an admittedly certain set of) domain specific languages (DSLs) ontop of it. Some people like Peter Norvig call Python a "successful real world LISP" (not a verbatim quote). In 2012, my application domains for Python were a mixture of scientific python (scipy/numpy) and web application programming (Django, Flask and friends). It didn't took long to make Python my go-to language for general-purpose scripting. This way, Python took over the position Perl had in before in my life. Perl has a somewhat different approach to syntax, often badmouthed as "write once, read never again" (Side remark: At that time, it was still Perl5 and Python2. The Perl community failed for the Perl6 transition while the Python community painfully made the Python3 switch over many years).
When Python introduced co-routines, things gone mad
In particular, Perl did not have a strong concept of iterators and generators. This is something Python3 emphasized even more then Python2. Generators are this kind of functions which yield
items while producing them instead of return
ing a list at the end. Generators are also called semicoroutines. They allow to re-enter a function multiple times. This can be helpful to avoid unneccessary memory allocations for intermediate values, effectively trading less memory demands for slighty more processing work (jumping between frames).
The concept of semicoroutines can be generalized to "full" coroutines, where functions can also jump into particular frames of other functions. The go-to use case for this kind of programming style are intrinsically "slow" I/O applications (in particular networking). Coroutines provide an easy mental model of interruptable code in a single thread, allowing the processor/code to do something while the slow operation is handled by the operating system.
You can think of the await
keyword introduced around Python 3.5 as a way of slicing the functions simililarly to the yield
keyword. However, real coroutines are significantly more complex to implement then semicoroutines. Therefore, Python language designers decided to mark real coroutines with the async
keyword. This effectively seperates the whole language and its ecosystem of libraries in two parts: The synchronous and asynchronous one. There were a lot of renowned blog posts about this concept such as Bob Nystroms What Color is Your Function? from 2015.
Coroutines are just one of the language features which can increase the mental burden for a programmer a lot, when digging into a new code base. Literally every concept such as generators or context managers subsequentally got their async counterpart, which can make codes insanely complicated compared to "good old easy sync python" world. And do not accidentally call an async python function the wrong way -- you won't notice until the runtime goes mad at some point throwing arcane exceptions.
I think python can profit from carefully written async code under the hood, but this is something one wants to expose only in carefully crafted framework situations. When using it, it feels like dealing with wild pointers in C. If you haven't worked within a few weeks with that programming features, you may forget all the specific weird syntax notations and special cases this style of programming requires.
Golang made my new Python moment
I recently had my new Python moment. This time, it was 2024 and it was with the programming language Go. The syntax is intentionally reduced to a minimum, supporting not even things like inline ternary operators. Just coming from a Rust project, this felt a bit like switching from Perl to Python 10 years before! I would draw this comparison even more: Perl and Rust are both more expressive, more versatile and more powerful at least in terms of syntax then their counterparts Python and Go. However, Rust also has "coloured" co-routines as Python does. Since Rust does not suffer from the famous global interpreter lock which renders CPython effectively single-threaded, a Rust async runtime (which is not determined by the language itself) can schedule co-routines amongst different threads. That makes it more powerful but yet mentally challenging, not even taking into account things the borrow checker dictate.
Go makes writing concurrent code incredibly easy. So easy that it is sometimes made fun of as a domain specific language for networking codes, because this is where it excels (in a similar way as Erlang). Instead of coroutines, which can easily result in a spaghetti-code program flow, Golang relies on goroutines which are rather lightweight threads with a clear message-passing information flow over explicit communication channels. This reduces mental load. Together with the right set of tooling out of the box, it is possible to write a TCP or HTTP server doing non-trivial things like proxying within a few minutes. I am by no means exaggerating -- my first Go code at https://github.com/anabrid/lucigo was written within a few hours in the night, as the git history also proofs. Coincidentally, I tried to write the same logic in Rust a few hours per week over a month without success.
Go definetly has it short comings. The compiler is far less sophisticated then the Rust compiler. The type system is not state of the art. For me it feels like Rust allows you to write very clever shared memory concurrent applications where the compiler ensures correctness, while Go makes it simple to write message passing concurrent applications. In High Performance Computing classes, it is common knowledge that message passing scales better, despite it can be less performant in the small scale.
And yet I enjoy the reductionism. As in python, there is typically one way of writing a program, one way of formatting it. In teams, this makes it easier to agree on the programming style and concentrate on the buisness logic instead. If you have complicated programs to solve, the programming language better get out of your way. Programmers attention is a limited good and Go makes an excellent job to demand it as little as possible. Go makes networking programming fun again.