My View on Embedded Programming Paradigms

The original title is “Which Programming Paradigm Should Your Project Use?”

My View on Embedded Programming Paradigms

Ah, a question from 1970, debated for nearly 50 years, and still so many conflicts.

Objectively speaking, for someone who has worked with quite a few languages and has used several paradigms in projects, the essence of these programming paradigms is a management issue.

My View on Embedded Programming Paradigms

01

Procedural programming is essentially “sequence, loop, branch”

Bring a primary school student over, explain it in ten minutes, and quickly deliver the project. Simple and clear, everyone is on the same page. But such a straightforward situation is basically impossible to see nowadays. Any decomposition of something, using the simplest procedural way, will lead to an explosive increase in complexity later on.

Procedural development is like someone always asking you for the follow-up plan, what to do next, and what to do after that. Unexpected events, interruptions, and crises; the complexity of everything today cannot be explained by just a few words of “sequence, loop, branch”.

Come on, dozens of tasks running concurrently, working in multi-threading; do you think using “sequence, loop, branch” to describe a person or a module is feasible? A single-threaded approach might work, but if it gets slightly larger, using such a simple description will either be nearly unusable due to excessive detail loss or so detailed that it exceeds human control.

This is a code structure suited for writing a small tool or toy; it’s fine for play but not for projects. Some special environments with extremely high stability and control requirements might still use it.

02

Object-oriented programming is essentially “inheritance, encapsulation, polymorphism”

It can be understood as modularizing everything; don’t ask me what you want to do or how to do it. Instead, calculate what resources you have, how many people, what each person can do, and what can be accomplished based on the current situation. As for what exactly needs to be done, the combinations are here, and when tasks arise, each is responsible for their own.

The upper layers avoid the details of the lower layers, keeping the understanding of what the intermediate layers interact with within a very small scope. For a long time, a large number of projects have almost exclusively used the object-oriented paradigm. The human brain can only process a limited amount of information at the same time; if too much information is received simultaneously, it either leads to forgetfulness or numerous bugs.

The structure of object-oriented code effectively achieves hierarchical levels and encapsulation; each layer only needs to understand the parts that need to interface, while other encapsulated details are not considered, effectively controlling the explosion of information within a small range. Each module and each person just needs to do their job, and everything runs naturally; when tasks arise, they get done; when there are none, they remain idle. However, when the project’s complexity exceeds a certain level, it can evolve into a completely unmanageable state of chaos. A model that emerged to simplify thinking has severely increased the overall complexity of the system.

The first problem: The cost of interfacing between modules far exceeds the cost of doing actual business.

Similar to a pyramid management model, once the number of modules exceeds a limit, the bottom layer that implements the business can barely write a few dozen lines. However, the upper layers’ interfacing, the upper layers’ upper interfacing, and various framework interfacing codes as middle layers far exceed the execution of business. In the end, all the clueless programmers are researching how to design structures, how to design frameworks, which abstractions to use, and how to abstract to simplify frameworks and structures.

The complexity of the middle layers and interfacing layers far exceeds that of actual business; no one, not even the most skilled architect, knows the details of the entire project, and no one can grasp it. Moreover, due to complexity and lack of understanding, it has become nearly impossible to modify.

This is also described by Amazon engineers: “My job is to dive into a mountain of shit every day, then go to the center of that shit mountain to find out where the problem is, and why this particular area smells so bad.”.

The second problem: Code bloat has inflated what could have been a few lines of actual code into dozens of times more.

Originally, there was something to do, and it needed to be completed. Because of the hierarchical division of object-oriented concepts, to implement the business, encapsulation is necessary, and it needs to interface well with the parent class. Multiple inheritance is the root of all evil, turning the entire system structure into a web or a ring, eventually becoming a tangled mess.

And single inheritance, which originally directly leads to the implementation of business code, now requires layer upon layer of interfacing and scheduling. Each layer of interfacing code has to be written by someone. Although it is mostly just pure typing with no technical difficulty, the increase in code volume, regardless of how simple the business or structure is, poses a significant risk of a hidden bug. After all, when problems arise, reading through to find issues also takes a lot of time.

The increase in code volume due to hierarchical division also leads to the third problem.

The third problem: Excessive indirect processes are required to access actual business, severely impacting performance and slowing things down.

It’s important to know that the first principle of “The Art of UNIX Programming” is the KISS principle, which is upheld throughout the book. (keep it simple, stupid!)

Writing projects and code aims to solve problems and create products that address these issues. However, the massive object-oriented model causes you to spend, or rather waste, too much time considering complex management and scheduling issues that are entirely unrelated to the problems you need to solve. Yet, although the object-oriented paradigm is the one most likely to lead to a shit mountain, to a large extent, regardless of the introduction of any architecture, the most commonly used method in our modern programming projects remains the object-oriented working model.

Everyone is in pain, everyone knows where the problem lies, and everyone is complaining about object-oriented programming, but after all, there is no better solution, is there?

03

Functional programming is essentially “function mapping”, colloquially known as rule-making

This function is not the same as the functions in procedural programming; this function is a mathematical concept. The definition is as follows: There exists a correspondence f between two non-empty sets A and B, and for every element x in A, there is always a unique element y in B corresponding to it; this correspondence from A to B is denoted as the function f(A).

This concept is quite difficult to understand; simply put, in our attempts to design countless language models to describe the operation of things and the execution patterns of complex businesses, and even to describe the world itself, no one has ever truly solved it; it always falls short because the human brain cannot keep up. The more massive and complex things become, the description of their complexity always spirals out of control.

Finally, starting in 1956, doubts began to emerge; in 1960, Lisp appeared, and in 1970, Lisp mixed the use of functional models, which at the time was still just a concept. John Backus formally proposed the concept of functional programming in his Turing Award speech in 1977. What functional programming does is simple: it abandons the ignorant human-created languages and concepts to use the language of mathematics that “God” used during creation. The complexity of this world far exceeds that of any computer software engineering. People began to attempt to understand and imitate the original structure of the world, writing code and constructing projects in this manner.

Let’s not talk too much about abstract concepts; in simple terms, the imitation method is to use “mathematics” to describe “rule-making”. Some may feel that computer programming is also writing using mathematics; what’s the difference between this and functional programming being singled out as “mathematics”? The most intuitive boundary between function description and statement description can be illustrated by a simple and familiar example.

The most classic way to write a math problem was: Let x1=3, x2=9, y=x1+x2, resulting in y=12. If we compare this to programming languages, we might feel this writing is verbose, turning into let x1 = 3; return x1 = x1 + 9; This may seem fine when accustomed to coding, but reverting back to the mathematical model is quite alarming. x1=3; x1 = x1 + 9?3= 3 + 9? The concept of a variable was introduced in the initial computer concepts: variables. The existence of variables allows data to be stored. This data storage is no longer a pure mathematical description.

The functional model describes how things operate, rather than the operation itself. The so-called how it operates is like writing a mathematical formula, inputting parameters, and outputting results. The essence is the function itself, not what parameters are inputted. The biggest difference in operation itself is referred to as “variables” in conventional programming concepts, while in functional programming, it is called “side effects”. This refers to whether the input data itself is treated as a function.

In conventional programming models, variables are part of the function; however, in mathematical concepts, the mathematical formula itself is everything and does not include the parameters inputted using the mathematical formula. These are more essential descriptions of functional programming, and just looking at this part is already much more complex than the previous few model concepts. These are merely surface phenomena; later, we will discuss how to solve problems, and what are the benefits and costs of discarding variables?

Computer program modules, due to the concept of variables, often do not split correctly after segmentation. Some initializations may not execute, causing subsequent modules to run independently, leading to issues such as missing data structures, etc., resulting in parts extracted alone being directly erroneous.

However, mathematical formulas, regardless of where they are cut from, any sub-formula remains correct. Sub-formulas can be brought into more complex formulas, ultimately yielding larger complex formulas. These processes, whether combining or extracting, always maintain the correctness of the formulas.

So, if we avoid such variable-style code like x1 = 3; x1 = x1 + 9, and maintain the high school problem-solving mode, let x1=3; x2=x1+9; bringing the first formula into the second, we get x2=3+9, resulting in x2=12. All code modules always maintain the input parameters, and the parameters are processed according to the formula to yield the result of this unique lambda expression model.

The entire program transforms into countless absolutely correct functions (formulas) and mutual interactions between functions. The system’s operation is simply inputting parameters to get results. There are no variable states, variable management, and numerous other issues. It’s like always establishing various rules, never specifying how to handle specific situations in certain cases; rather, it allows the rules to remain correct, and everything else just needs to operate naturally under those rules without requiring any interference from so-called middle management.

Using this code design pattern, the first characteristic is that it’s mentally taxing. A classic example is the following code, which returns the sum from x to y, where x<y.

function test (x, y) {

let res = 0;

for(let i = x; i <= y; i++) {

res=res+i;

}

return res;

}

This code violates the functional programming model; the variable res is a side effect parameter, and i is a side effect parameter. Setting res=0 at the beginning, the model of res=res+x has already been overturned. If we extract any process from the middle, it will already be incorrect. Without introducing variables, the concept of loops in conventional programming models becomes impossible to implement. Our approach is:

function test_run (x, y) {

return test2 (x, y, x);

}

function test2 (x, y, res) {

if(y ===x) {

return x;

}else {

console.log(x, y-1, res+y);

return test2 (x, y – 1, res+y);

}

}

test_run (1, 100);

This function, aside from x and y, has no variable factors. This is the ideal state for “rule-making”. No matter in what state, no matter at which step of the system’s operation it is extracted, this formula is always correct; this is the so-called functional model that imitates the mathematical model.

The above is no longer a loop, as the fundamental basis for the existence of the loop concept is variables. Variables as counters are necessary to sum from x to y. This is a tail-recursive function.

It’s important to recognize that tail recursion and recursion are fundamentally two different things, and tail recursion can replace iterative loops, while recursion cannot. Memory bloat can be severe, even though tail recursion looks very much like recursion.

Adopting functional programming greatly reduces system complexity and operational costs. Because there is no so-called runtime, there are also no runtime errors. All middle layers, hierarchical levels, and management modules can be eliminated. As long as the established rules are followed and not exceeded, minimal management is required. The setting of formula rules should not involve detailed changes in the actual operational process; it should not specify how to handle situations in which certain conditions apply. There can only be inputs and outputs along with the logical operation of the formulas; parameters can appear, but side effect parameters are not permitted. It can only be written in a manner similar to let x2=x1+9; x3=x1*x2.

Only establish reasonable, effective, and correct specifications, without getting entangled in the specific operations under the rules. The rules themselves are already the best management.

The second characteristic of functional programming is: As long as you ensure the function is correct, the function runs correctly, and the complex functions composed of functions are correct, you do not need to worry about what the function is doing; you only need to know what parameters it takes in and what it outputs, and you can create any mode of function using any multi-threading or multi-calling methods. Because there is no concept of variables, there are also no critical section issues, which are the most troublesome in multi-threaded programming.

The third characteristic of functional programming is: Speed. There are no middlemen profiting, no middle layers messing around; resources can be fully utilized where they are needed, resulting in fast execution.

The fourth characteristic of functional programming is: speed. This speed refers to writing code quickly. In reality, when we write projects, we are not just writing business logic. Business code can be completed in two or three days; then comes structural design, framework design, interface design, data management, memory management, scheduling management, resource management. These are all manageable, but when bugs arise, 10% of the time is spent writing bugs, while 90% is spent debugging them. (Are you hired just to write bugs for our company?)

Any module in functional programming without side effect parameters is a formula unrelated to the data itself. As long as the smallest module you write is correct and you haven’t made a logic error, it won’t go wrong.

There’s a saying that when developing a project, C++ and Java take a year, C takes five years, while Lisp might only take three months. This comparison is a bit exaggerated, but in terms of code volume, the magnitude is about the same.

Doesn’t it feel like functional programming is the future, the trend? Yes, it is a trend, and we certainly need to learn what we should learn. But now I’m going to contradict myself.

If functional programming is so useful and effective, and if it has no fatal flaws, why has it not become more popular after over 50 years? The prevalent paradigm remains object-oriented.

Disadvantage 1: The threshold is too high, which means you can’t recruit people.

Object-oriented paradigms emphasize collaborative development; each person only needs to focus on the issues at their level, simplifying the amount of information received by encapsulating the underlying operational details. In a well-structured setup, distribute architectural documents, and multiple people can develop simultaneously. You’re excellent, but your skills are lowered; encapsulated in the code layer. If your skills are low and can’t pass TDD tests, as long as you can pass, there won’t be any major issues; this is a work mode that can be mass-produced, allowing a large number of cheap labor to create a usable mess. It doesn’t require highly skilled individuals who need a broad skill set.

Only one object-oriented architect is needed to grasp the overall picture; others just need to look at the documents, mindlessly pile code, and pass tests. Stacking repeated workloads hundreds or thousands of times, if it doesn’t run, waste resources, just throw money at it.

It is important to know that throwing money at hardware is visible and honest. One dollar spent yields one dollar of performance; it doesn’t need to be written perfectly. However, human existence comes with countless uncertainties; using money to represent ability is difficult, increasing algorithm complexity, making it incomprehensible to those with lower skills, leading to various problems; it’s better to just throw money at hardware.

Lowering the skill level has another benefit: it’s easy to recruit, and everyone can replace each other; the difficulty is not high. Finding someone who can work overtime is more useful than finding someone highly skilled.

The object-oriented development model is very suitable for commercial operations. However, the threshold of functional programming is just two words: “mathematics?” Hehe.

function test_run (x, y) {

return test2 (x, y, x);

}

function test2 (x, y, res) {

if(y === x) {

return x;

}else {

console.log(x, y-1, res+y);

return test2 (x, y – 1, res+y);

}

}

test_run (1, 100);

Just this simple thing, a straightforward addition from 1 to 100, can already be seen as a loop, and it’s probably hard to recruit people.

Disadvantage 2: Slow.

The previous mention of speed now seems contradictory. What I mean by slow is the performance degradation and resource consumption during runtime. For example, neural network algorithms are very popular; how many really understand what neural network algorithms are? This term isn’t as mystical as it seems; simply put, the essence of neural network algorithms is polynomial decomposition.

The most common polynomial decomposition is f(x)=an·x^n+an-1·x^(n-1)+…+a2·x^2+a1·x+a0; this is something everyone has learned, so there’s no need to introduce it.

Polynomial decomposition uses n-th power polynomials to describe any function and can decompose any function into infinite polynomial functions of varying degrees. Neural networks can use weighted algorithms to describe any function and can decompose any function into an infinitely layered polynomial function. But why not use polynomial decomposition learned in middle or high school? Why introduce such a complex neural network that’s so popular?

To be blunt, while computers seem powerful, we all think CPUs are strong. Those studying computer science know that the design of CPU registers can only perform limited tasks; they are not powerful at all. Polynomial decomposition is something you can teach a middle schooler to calculate, and now it seems that it’s even taught in elementary school. However, when you ask a computer to calculate polynomial decomposition, it uses countless tricks to abstract into an approximate multi-level power operation or multi-level sin, cos operation.

Neural network algorithms, while appearing to have more complex input-output weight operations than polynomial decomposition, are actually much easier for computers to calculate than polynomial operations, which are unquantifiable in terms of cost. Writing code purely from a mathematical perspective, introducing certain mathematical concepts can lead to excessive additional work when running on computers because they are not adept at it, and the results may be approximate and never accurate.

In a sense, the concept of “floating-point numbers” used in computers is already an approximation, which can never achieve the accuracy of mathematical calculations. If you avoid the floating-point concept with other code, the complexity and resource consumption will start to skyrocket. Additionally, the limitations of registers such as 2^32 or 2^64 for large number calculations, even simple addition and subtraction can be a highly complex operation. These issues would not need to be considered in true mathematical calculations.

Moreover, computers do not actually perform division calculations.

My View on Embedded Programming Paradigms

Our current world’s network security foundation lies in the RSA algorithm, which no one can crack. It’s certainly not that it can’t be cracked; it’s just that computers cannot perform division calculations. The product of two large prime numbers and the division of that product is a world problem. Some things can be truly terrifying if you think about them; can you calculate division? What is division? Is your understanding of division just memorizing a multiplication table and then reversing it for division calculations?

In a sense, if you truly apply mathematical concepts, having a computer with a low-level CPU calculate powers, large numbers, dozens of lines of code can potentially crash a super server. Any concept you think is simple can become a complex algorithm, and resource consumption may unexpectedly lead to deadlocks.

Disadvantage 3: Slow.

This slowness corresponds to the previously mentioned development speed and code-writing speed.

In essence, it still boils down to the threshold being too high. You can hire a group of average object-oriented programmers, design a structure, and they can quickly code thousands or even tens of thousands of lines, completing the task. In terms of code volume, it can be massive, solving a small issue with low efficiency. In reality? You find a functional programming expert who sits down to think, writes, deletes, tests, and modifies, completing the project in just a few dozen or a hundred lines. Is this development efficiency high? And it can be quite interesting, requiring a high degree of mental engagement?

But what if they haven’t thought it through? What if this person has personal issues at home? What if they fall ill? What if their skills aren’t as impressive as they seem, and they’re struggling to come up with ideas?

If the entire team has high skill levels and a solid foundation, project development efficiency will be high; however, the reality is that it’s nearly impossible to find such conditions. Even if you do find them, it won’t matter; this leads us to the fourth disadvantage.

Disadvantage 4: The Lisp Curse (The Lisp Curse)

This term has been discussed in many articles and has been a topic of countless debates over the decades. It’s a concept that is somewhat complicated to describe, and I am convinced that even if the general skill levels of programmers improve and functional programming becomes more excellent, it will always remain a niche and is not recommended for widespread use in projects for the most important reason.

The most accurate description of the Lisp curse, which is also the easiest for the public to understand, is as follows. Lisp is so powerful that technical issues in other programming languages become social issues in Lisp.

Yes, you read that correctly; it’s not a technical issue, but a social issue. The biggest distinction between technical and social issues is “people”. The technology is fine and very powerful, but a strong technical model inevitably leads to human division, community division, and ultimately, the functional development model will die due to “management issues” unrelated to technology. The most serious problem caused by the Lisp curse is “lone wolf development”.

Let me give you an example: Object-oriented programming is merely a design thought, a concept; it does not imply that C++ is an object-oriented language or that Java is an object-oriented language.

Even C can be an object-oriented language; the Linux kernel is natively written in GNU C89 with object-oriented principles. However, to support object-oriented development patterns, the Linux kernel has written a lot of concepts to maintain modules, maintain struct function pointers, load kernel drivers, and so on. In contrast, C++ and Java directly add a bunch of syntactic sugar to the compiler to enhance object-oriented writing.

However, how many lines of code are needed to write a complete set of object-oriented concepts and frameworks in Lisp or Haskell under a purely functional programming model? Just a few hundred lines.

This development advantage ultimately leads to countless low-quality, unmaintained, bug-ridden object-oriented frameworks being written in the community. Because of this, you can only write your own object-oriented framework; as long as your theoretical knowledge is solid, it’s likely you can write it in just a few hundred lines, taking just a day or two.

Everyone is using their own written object-oriented frameworks; each person is using their own library, and there are gaps between them. Those at a higher level can write several sets of dependencies for their projects without much difficulty, but they won’t be used by others. Over time, they can easily disappear. When handing over projects, countless unmaintained, personally developed dependencies, which are small in code volume but powerful in functionality, become too complex for others to manage. Project handovers become almost impossible unless a top-notch developer takes over, but similarly, if this is not the case, once the technology gap appears, the project becomes incomprehensible to others. The best solution isn’t to gamble on finding someone; it’s to delete and rewrite the parts that are incomprehensible but must be modified.

Ultimately, the overly complex logic of functional programming will always lead to a gap in skilled personnel; after all, abilities vary widely, and the high initial technical threshold makes it hard to bridge. Projects that can still be maintained are almost always those written and maintained by one person; even the presence of a second person may only be temporary. “Lone wolf development” is a very apt name.

In conclusion, as discussed in the classic software project management textbook “The Mythical Man-Month”, “there is no silver bullet.”

In what situations should which patterns be used? Don’t be too confident; don’t believe too much in the development patterns and frameworks you are accustomed to using. If you only know one pattern, how can you compare? If you only know one language, how can you compare?

My View on Embedded Programming Paradigms

Stop arguing; let each find their own place.

In driving development and embedded low-level development areas, procedural programming models are clean, efficient, intuitive, and offer high resource control. In these environments, procedural development is almost irreplaceable.

In environments with a large workload, lower difficulty, excessive details, and where simple normative rules cannot cover everything, use object-oriented development models to churn out industrialized projects with low-quality manpower.

In environments where complexity is too high and the increased interfacing complexity of object-oriented models is nearing chaos, deleting the upper middle layer code and allowing a few elites to use a standardized functional model can significantly simplify project complexity and development costs.

There’s still a lot to learn; the pursuit of knowledge is endless!

My View on Embedded Programming Paradigms

My View on Embedded Programming Paradigms

1. This is what university education should look like!

2. “Xuan Tie” is followed by “Wu Jian”: Alibaba Tsinghua releases AIoT chip platform, aiming to lead a new model for chip development.

3. Are traditional embedded C programmers about to become extinct?

4. Huawei sends signals: If ARM is restricted, consider using RISC-V architecture.

5. The hardcore microcontroller programming thoughts of an embedded engineer!

6. To make embedded development more efficient, have you prepared these tools?

Leave a Comment