Subroutines Are Not Functions
The need for multiple programming languages not based on functions.
A CPU is just a chip. A CPU imparts a certain meaning to data stored in memory. CPUs use a string of periodic electronic pulses - called a “clock” - to sequence their activity.
A CPU interprets data1.
The ideal language for programming a CPU is - assembler.
At some point, we began using a textual notation that looked a lot like math, laid over top of assembler. IIRC, the first versions of FORTRAN used the word “subroutine” instead of the word “function”. The first versions of FORTAN did not support recursion. Early CPUs did not have stacks. Stacks were added later.
Subroutines are not functions. You can fake out the function-based paradigm using subroutines, but, that is not what subroutines were designed for. Subroutines were designed to save space (code space) at the cost of extra CPU cycles. Subroutines are non-reentrant and are not thread safe. To fake out the functional paradigm, you have to add software2 to the system to implement reentrancy and thread safety.
At some point, the math-like notation laid over top of assembler began to be worshipped as math itself (The Gods Must Be Crazy).
Almost all programming languages, except, say, Prolog and its descendants, use the function-based paradigm3. Most languages provide the concept called “function”. Functions are sometimes pure and sometimes impure. All functions in programming languages are simulated atop non-reentrant CPUs. Simulating the functional paradigm involves using a huge, bloated, lump of software commonly called an “operating system”.
Monads, yields, futures, promises, etc., are epicycles added to the function-based paradigm, to extend the stretch of function-based notation into realms that the notation is not suited for.
Functional notation worshippers make up crazy edicts for programming like “no side effects”, “no mutation”, etc. These edicts cause cognitive dissonance in programmers who understand the hardware. After all, the fundamental operation of CPU chips is to mutate memory cells.
In contrast, Prolog’s syntax looks like functions, but represents something else. For example, you can put CONS operations and destructuring operations into the part of the syntax that looks like parameter lists. Prolog fakes out a different paradigm using CPU chips and electronics.
Prolog’s engine simulates a relational paradigm.
Most popular programming languages, like Python, Rust, Haskell, etc., all simulate the same paradigm - the function-based paradigm.
Both paradigms - function-based and relational - are implemented atop lowly CPU chips using subroutines that were not designed to support either paradigm directly.
Divide and Conquer in Physics
A fundamental lesson in the field of physics, is that of devising simplifying assumptions. This is just the technique of divide and conquer.
In physics, you are taught to focus deeply on areas of interest by reducing the number of variables to be considered and by pushing aside all details that don’t affect the area of interest.
Fundamental lessons in physics consist of learning how to calculate which details can be ignored and pushed aside. These lessons involve notations like “>>” (much greater than). If an area of interest has effects that swamp out other effects, then physicists can make the simplifying assumption that the swamped-out effects don’t matter, at least in the first order.
Physicists are, also, taught not to forget what the simplifying assumptions are and to detect when their analyses come “too close” to the edges of the assumptions for comfort. If a simplifying assumption becomes invalid, because it brushes too close to ignored effects that do matter, then physicists stop using the assumption and switch to another set of assumptions and notations.
Switching Notations in Programming
It is my observation that programmers continue to use one notation - the function-based notation and paradigm - long after the notation shows signs of stress, of moving out of its sweet spot.
For example, the function-based paradigm is meant for expressing computations - calculations where only the calculated result matters. This kind of thinking uses the simplifying assumption that time doesn’t matter. This is OK for building calculators and very complicated calculators, like military ballistics compute-ers, but, it is NOT OK for building things that involve time.
What kinds of problems involve time? Just about everything we want to think about these days, like:
Video and audio sequencing (iMovie, Flash, DAWs, etc.)
Robotics
Gaming
GUIs
Blockchain
Internet
Searching databases
Parsing text
etc.
Solving these kinds of problems would be better served by use of different notations - programming languages.
For example, doing exhaustive search in the functional paradigm means creating towers of code that use loops and recursion. On the other hand, doing exhaustive search in the relational paradigm, i.e. Prolog and friends, is - easy and straight forward.
The concept of yield has been bolted into the functional paradigm, but, it is more easily expressed using the concept of state machines, embodied in a notation like Statecharts. Statecharts have been around for a long time4. In fact, Statecharts don’t fundamentally need the extra bloatware - operating systems - for faking out a functional paradigm.
Composing Paradigms Together To Make Programs
Can you use multiple paradigms for building programs? Can you express one part of a problem in one paradigm and another part of the problem in another paradigm. Then, bolt the two parts together to make a larger program?
Yes.
In fact, we see this kind of thing all of the time in, say, Unix pipelines. Unix pipelines are composed of commands - smaller programs. Commands can be written in any programming language (aka paradigm), as long as they input and output data in a format compatible with other commands in the pipeline.
Can we do better?
Yes.
Unix commands and pipelines are but primitive attempts at bolting different paradigms together, using a primitive data format5.
Unix pipelines use operating system processes and all of the attendant hardware and software, like MMUs, preemption, etc.
When you boil this Unix stuff down, you can see simple patterns emerge:
Closures are simplified processes without the need for hardware bloat, like MMUs.
Communication between closures can be done using FIFO queues.
Sequencing - control flow - of commands can be done by simple dispatcher subroutines.
The worst thing about Unix pipelines is the syntax. The syntax is very limiting and makes it hard to express multiple connections and feedback connections. I favour the use of diagrams for breaking free of this syntactical limitation, but, maybe you can think of something better.
Sector Lisp
The epitome of the function-based paradigm is embodied in a tiny language called Sector Lisp6. Sector Lisp uses 436 bytes to implement the function-based paradigm. Sector Lisp’s Garbage Collector is only 40 bytes long. Sector Lisp is implemented in pure assembler using lots of low-level, space-saving tricks. The use of assembler and of assembler tricks clouds the real reasons for why Sector Lisp is so small - it sticks to the knitting. Sector Lisp is based on pure functional programming.
If your functional programming language is larger than 436 bytes, ask yourself why. Is it because that language has lots of out-of-band capabilities forced into it? Is the function-based paradigm being stretched to the breaking point, is it being pushed beyond its sweet spot? Should you switch to using more languages/paradigms for solving your problem?
IDEs
Programming languages are 1950s programming IDEs7.
In 2024, essentially one kind of programming idiom still forms the backbone of IDEs for programming. Baubles, like syntax colouring, have been added, but, it’s still the same-old-same-old. One kind of paradigm for all of programming. Even if you use OOP, you’re still using the same basic function-based paradigm.
Can we do better?
Yes.
Should we do better?
Yes.
Are we doing better?
No.
See Also
References: https://guitarvydas.github.io/2024/01/06/References.html
Blog: https://guitarvydas.github.io/
Videos: https://www.youtube.com/@programmingsimplicity2980
Discord: https://discord.gg/Jjx62ypR
Leanpub: https://leanpub.com/u/paul-tarvydas
Gumroad:
https://tarvydas.gumroad.com
Twitter: @paul_tarvydas
To emphasize this point: A CPU is an interpreter. That means that compilers are interpreted.
Aka “inefficiency”. The extra software comes in the form of context switching and code that handles preemption. After all, functions insert ad-hoc blocking into the code. The caller blocks (suspends operation) until the callee returns a result. How deep does that go? You can’t know without deeply digging into the code. Can you run out of physical memory by faking out recursion using a lowly CPU chip? Yes. Operating systems want all control of blocking, so, they use full preemption to wrench control away from code.
Note that languages that are heavily slanted towards different paradigms, like OOP, still use the function-based paradigm as their basis.
Harel wrote the paper in 1986. I read and explain the paper in https://guitarvydas.github.io/2023/11/27/Statecharts-Papers-We-Love-Video.html
The format is: lines of text, delimited by the special character \n. That format is much too complicated and too restrictive.
https://justine.lol/sectorlisp2/
Integrated Development Environment.