Software is Evil

Software is Evil

Software is evil—it has two faces: composition and behavior. One reflects the author's view of how best to structure things, whereas the other reflects how best to act. This duality, rather, the inability to recognize it, is evil.

Software is evil—it has two audiences: computers and humans. Computers comprehend intent with no biases, whereas humans comprehend intent despite biases. This duality, rather, the inability to recognize it, is evil.

?Intent. Or, rather, the inability to convey it is the root of all evil.

Conveying intent is hard. Conveying intent in software is even harder. Sadly, we don't do it very well. One should not have to question what something is or what something does; it should be self-evident.

We form ideas as we read—a complex act of perception: the more expressive and solitary the idea, the more accurate our perceptions. Once misconceptions are introduced, intent is lost, and software's fate is sealed.

We struggle with naming things and defining relationships clearly, relating events that must flow in a particular way or not if exceptions occur. We fail to see that writing software is indeed writing. It has elements of literature—plot and style.

Interlaced and equally important are traits of good writing: clarity, cohesion, convention, and organization. Great literature is the culmination of these qualities–it has excellence of form; is easy to understand, expressive, and efficient in its delivery.

Like literature, software is an author's storied solution to an end. How well the reader (human or artificial) comprehends it depends on how well an author conveys intent and inspires empathy. ?

Expression of intent is the burden of literature, and clarity is the burden of its authors. If the intent of our writings is not clear, we have failed as authors. Clarity promotes predictability; it is fundamental[1] to how we think and vital as we guess consequences just by reading.

"We spend more time reading code than writing code."

We think differently when reading than writing code. When writing, we're trying to express our intent to solve a problem; when reading, we're trying to comprehend a problem by predicting intent.

Predictability presumes that an observer can foresee future states of a system given its current state and the set of functions that mutate it at arbitrary points in time—causality.

Alas, we are limited by our capacity to comprehend. If our predictions are not equal in scope to the complexity of any/all plausible effects, we risk mistakes when things change.

In literature, predictability proves a plot. It unfolds through a pattern of events, one paragraph or verse at a time. In software, it unfolds through a sequence of events affecting state one statement or function at a time.

An author's affinity to a plot is crucial—having more than one is unfitting and leaves the reader confused and apathetic. A plot governs the exposition of a story and the structure of statements that compose it. Although contrived, the acronym plot (physical, logical, organization, and style) conveys software's composition.

Disclaimer

Admittedly provoking, I genuinely believe that software is (largely) evil—a viewpoint cultivated over many decades of programming. Many will consider this viewpoint absurd. They see software only as a collection of statements executed by an autonomous machine.

Early computer scientists saw software the same way, as a tool to solve mathematical problems. Written in an algorithmic format, this model is well suited for such sequential tasks.

Modern software is not sequential nor autonomous; it is an interactive machine that detects and effects changes in its environment; it exhibits behavior and is communicative–causality.

Sadly, the sequential model is so ingrained in the collective mind of the software community that most do not recognize it. They ignore it as a whimsical notion, cling to traditional ways, and are cursed to repeat the fateful act of rewriting a system when it becomes fragile and ridged.

The notion that software is nothing more than a collection of statements is a conceptual disaster. Those who nurtured this viewpoint are too many to cite. Still, I'd be remiss not to mention Grady Booch, Robert Martin, Martin Fowler, Edsger Dijkstra, and the 'Gang-of-Four.' Their fingerprints are everywhere.

Overview

In software, things belong on one of two piles: things responsible for behavior and things responsible for composition—things that change state and things that do not.

Composition is the act of designing and constructing structures—a notion referring to patterns and relationships amongst its parts—architecture.

Behavior is the range of actions made by entities in union with themselves and their environment—cause and effect.

Mixing these two is almost always a bad idea; it introduces improper dependencies making code hard to reason about, tightly coupled, and virtually impossible to test.

In object-oriented programming, a prime culprit is a new operator lurking in an object's constructor[2]collaborators.

Collaborators (are frenemies)

Collaboration infers a relationship and occurs when one is coupled to the state of another. A collaborator is friendly when it imposes no liability, like creation or initialization; otherwise, it is adversarial—tightly coupled.

Mixing composition and behavior violates the Single Responsibility Rule (SRP); it imposes a concern different from an object's intent; it breaks the seam from which improper dependencies should not propagate, making extensibility and testability impossible—the Open-Closed Principle (OCP).

Markedly, things responsible for composition are mainly object-oriented (OO), whereas those responsible for behavior are mainly functional (FP).

"OO makes code understandable by encapsulating moving parts."
"FP makes code understandable by minimizing moving parts."
~Michael Feathers (Twitter)

Object-Oriented Programming

Object-oriented programming is evil, an imperative style that expresses logic explicitly and mutates state. It takes a deep understanding to apply today's object-oriented concepts correctly[3]. Its current principles are flawed, and there is no support to enforce the best practices required to compensate for them.

Abstraction (the promise to disguise complexity)

"All problems in computer science can be solved by another level of indirection… except for the problem of too many levels of indirection" ~Butler Lampson

Abstraction lets us compress or omit details in favor of concepts; to swap complexity in favor of named[4] notions. It lessens complexity by compressing it—like swapping a light bulb's concept to convert electricity to visible light.

Defining the correct set of abstractions is key to object-oriented design. As changes in them force changes in whatever depends upon them. Getting this wrong is often fatal—the impact or inability to make widespread changes, as seemingly unrelated code depends on exposed details—leaky abstractions.

Leaky abstractions are evil. They cause code to become brittle and fragile. Sadly, most (non-trivial) abstractions are leaky—they leak in favor of performance. This (inadvertently) increases complexity as its original intent is lost amongst (false) abstractions that are nurtured to "make things work."

Ultimately, code becomes incomprehensible and breaks under the weight of tightly coupled code.

Encapsulation (the promise to hide complexity)

While abstraction centers on observable behaviors, encapsulation centers on (hiding) the implementation of those behaviors; in this way, an abstraction has two faces: its interface and its implementation.

In theory, the interface is where an observer can assert assumptions by reasoning about its inputs and outputs. In contrast, the implementation is where an observer must assert no assumptions nor reason about it, lest it lies about hidden side effects or dependencies.

In theory, encapsulation protects the implementation against inconsistencies, sometimes referred to as invariant violations[5]. Such assurances are vital in coping with complexity. In practice, pure encapsulation rarely occurs; it is forfeited for performance—passing objects by reference or by exposing state.

The moment this happens, encapsulation is broken, tight coupling occurs, and side effects emerge. A function has side effects if it:

  • Mutates shared state—anything outside the scope of the function
  • Mutates input arguments—precisely, a referenced object
  • Throws an exception[6]
  • Performs any I/O operation

Inheritance (the promise of reuse)

Inheritance, in object-oriented languages, affords compressed definitions of new types that extend or override state—implementation inheritance. Compression in that new definitions can (mostly) ignore details of what is inherited.

Inheritance, in object-oriented languages, affords comparable definitions of new types that override behavior—interface inheritance. Comparable in that new, derived definitions can substitute behavior.

Inheritance is evil; it depicts two subtle types: implementation and interface, called sub-classing and sub-typing.

Inheritance[7], as an abstraction, is a lie. Worse, it breaks encapsulation[8]. It fails when the hierarchy is too deep or hidden. When the context needed to make predictions about code is too large or disjoint[9], changes to underlying layers unexpectedly affect other behavior.

Inheritance is evil; it forces early binding—a direct and static relationship between tightly coupled objects. To reuse anything, one must reuse everything.

In practice, (true) inheritance is rare. What appears natural quickly becomes an irregularity as varying, erratic usage pollutes software with false abstractions.

Polymorphism (the promise to reduce coupling)

Polymorphism is the one true value of object-oriented languages[10]; it allows us to override behavior and manage dependencies. Sadly, it too adds complexity as intent becomes less clear.

Summary

Object-oriented Programming (OOP) was conceived with the (false) promise to control the complexity of procedural code and improve its organization. Sadly, these have proven to be empty promises:

  • There is no guarantee of getting the same result given the same input.
  • It encourages the use and mutation of shared state.
  • It introduces choice, a subjectivity that demands interpretation, inviting inconsistency and volatility.

Worse, the practice of sharing (mutable) state causes coupling.

"Tightly coupled code is the single most destructive force in software."

Coupling (is evil)

Coupling occurs when encapsulation is broken—leaky abstractions. Tightly coupled code inflicts improper dependencies, making code immobile, rigid, and fragile. Ultimately, it ushers in a software apocalypse:???

When code is not easily sharedimmobility; sharing code means sharing its dependencies and side effects, even if not relevant to what's being reused.

When code is prone to propagationrigidity, the accumulated effect of immobility, each change causes modifications somewhere else. What starts as a small change grows into widespread revisions.

When code is prone to breakagefragility, the accumulated effect of rigidity; widespread breakage occurs for each change—often in areas with no conceptual link with the changed location. Every change causes more problems than it solves.

Each is a symptom of improper dependencies. Introduced by failing to create boundaries across which they should not propagate. The primary purpose of (most) design principles and practices is to mitigate dependencies.

The primary purpose of the Open-Closed Principle (OCP) is not to propagate dependencies into existing code.

The primary purpose of the Liskov Substitution Rule (LSP) is to not propagate dependencies in derived classes that do not exist in its base class.

The primary purpose of the Dependency Inversion Principle (DIP) is to create boundaries across which dependencies do not propagate.

The primary purpose of the Interface Segregation Principle (ISP) is to inhibit the propagation of improper dependencies through interfaces (seams).

The primary purpose of the Law of Demeter (LoD) is to decrease the scope of dependencies.

Exceptions (are evil)

An exception is a side effect! Why have programmers banished[11] the goto but not the throw?

  • A throw disrupts normal program flow.
  • A throw makes a function dishonest if its signature returns a value
  • A throw makes a function leaky as one must look inside to know what is thrown. One must travel in code to understand all paths between throw and catch to understand exception handling. To remove either changes behavior!

Choice (is evil)

A common practice is to return null from a function. This practice introduces choice—choosing what a null value means and how to handle it.

If done incorrectly or ignored, an exception occurs. There is no guarantee that an exception is caught or handled consistently when an exception occurs. Or even if anything can be done about it.

Choice involves interpretation; subjective interpretation invites inconsistency, which introduces unpredictability. Choice makes writing predictable software hard. In software, freedom from choice is often better than freedom of choice.

Conditionals (are fickle)

Control flow occurs when executing conditional[12] statements or evaluating expressions—a necessary decision process. Yet, they harbor most[13] bugs in software. They violate its temporal integrity, making predictability of behavior and timing all but impossible.

Software looks static—a spatial concept of statements, functions, and classes. Yet, its true nature is dynamic—it unfolds over a series of time instances of execution. At any point, state can (and likely will) be different—temporal integrity.

The mental gymnastics required to accurately predict varying states in time by reading static text alone makes software evil. Conditionals make code harder to read[14], modify and test; they suggest that code does more than one thing.

If something does more than one thing, you must test more than one thing. If things change in more than one way, it becomes the burden of the programmer to reconcile those changes in relationships affected by them—assuming, of course, that they even know about them.

Note: conditional-free code, although preferred, can increase code surface through polymorphism or inheritance, which can be equally bad.

Comments (are lies)??

Comments, over time, are lies. As programmers are untrustworthy, comments are not trustworthy to reflect changes.

"No comment is better than a comment that is no longer true."

Comments are often warning signs that code is too complex, has improper dependencies, or is vague. The urge to write comments hints at a failure to express or conceptualize an idea in code[15].

Names (are misnomers)

"There are only two hard things in Computer Science:
cache invalidation and naming things."
~Phil Karlton

Creating meaningful names is hard. To name it, you must understand it. If done incorrectly, you risk confusion and deception. Intent should be self-evident by the name alone (or function signature)—no more and no less than what its name infers—a promise to be:

????Telling; implies what it is or why it exists, not how its implemented

????Exclusive; don't use the same name for more than one thing and don't give the same thing different names in different places; mixing terms leaves the reader left asking: what is the distinction? Do I care? Should I care?

????Pronounceable; if you can't pronounce it, you can't discuss it

????Searchable; single-letter names and numeric constants are awkward

????Memorable; use domain-specific words; avoid humor—It's likely not funny

Consider the following excerpt from "The Looking-Glass" by Lewis Carroll:

"When I use a word," Humpty Dumpty said, in a rather scornful tone, "it means just what I choose it to mean—neither more nor less."

"The question is," said Alice, "whether you can make words mean different things."

"The question is," said Humpty Dumpty, "which is to be master—that's all."

Alice was much too puzzled to say anything, so after a minute, Humpty Dumpty began again.

"They've a temper, some of them—particularly verbs: they're the proudest–adjectives you can do anything with, but not verbs; however, I can manage the whole lot of them! Impenetrability! That's what I say!"

"Would you tell me, please," said Alice, "what that means?"

"…I meant by 'impenetrability' that we've had enough of that subject…."

"That's a great deal to make one word mean," Alice said in a thoughtful tone.

"When I make a word do a lot of work like that," said Humpty Dumpty, "I always pay it extra."

"Oh!" said Alice. She was too much puzzled to make any other remark.

Primitives (are guiltless liars)

Primitive types are guiltless liars—they are arbitrary values, not abstractions. Using them as function arguments makes code hard to reason about. For example:

No alt text provided for this image

It is not possible to reason about the CalculateRisk function. It is dishonest. It does more than one thing: it calculates a Risk factor if the specified age is valid; otherwise, it throws an exception. Worse, it decides what a valid age is. If age is used elsewhere, this validation is likely duplicated—duplication is a sign of mixing logic and data.

Functional Programming

Functional programming is evil, a declarative style that expresses logic implicitly and clones state at the expense of performance (memory & speed).

Functional programming is evil; it suffers:

  • Cloning: expensive in terms of processing time—state is never mutated in place; instead, it requires creating a new version.
  • Input/output (IO): intrinsically non-functional; you can only isolate it.
  • Terminology: a vocabulary rooted in mathematics (non-intuitive)
  • Recursion: expensive in terms of (stack) memory usage and clarity
  • Composing: combining functions to form complete systems is often tricky.

Stateless (the promise of autonomy)

State is a necessary evil. In the functional programming style state is never mutated, only replaced. This is (often) expensive in terms of memory and speed. For example, replacing for-loops with recursive functions eliminates mutating control variables but increases stack space.

Still, any useful system must eventually do something. It must cause a side effect; however, there is a distinction between intended and unintended side effects. An intended side effect results from the behavior of an application, whereas an unintended side effect results during the act of that behavior.

Notably, the consequence of any side effect is a factor of distance. The further something can see, the greater its effect. This scope of mutability and its disastrous effect has led to the distinction between pure and impure functions.

In pure functions, the output is based strictly on its input arguments and causes no side effects. The same output is guaranteed given the same inputs, making pure functions easy to reason about, optimize, parallelize and test.

In impure functions, the output is affected by factors other than its input arguments and may cause side effects. It prohibits optimizations:

????Parallelization—carrying out two or more simultaneous tasks

????Lazy evaluation—optimization through avoidance

????Memorization—caching

The promise not to change state is imperative in a declarative style.

Immutability (changes everything)

Immutability changes everything—Pat Helland. Conceptually simple; however, it has subtle variations:

  • Write-once: named fields defined with the readonly or const modifier are write-once immutable; the compiler ensures it cannot change once initialized.
  • Shallow: a particular case of write-once immutability; whereby the value of a readonly field cannot change; however, if the field is a reference object, you can change what that field contains.
  • Temporal (Popsicle) allows mutation until the object is frozen; however, there is no compiler-enforced pattern.
  • Observable: if a property returns the same value each time, it appears immutable; however, if the underlying implementation is doing lazy initialization, it might not be immutable.

Summary

Functional programming was conceived with the (loose) promise to make robust, compact, and easily parallelizable software. A declarative model to describe computational logic without describing its control flow.

To exploit modern, multicore systems, software must embrace concurrent and parallel programming models. To do that, software must adopt a style that promotes them. The imperative style is based on explicit control flow and sharing state, which makes implementing these models difficult, if not impossible.

Yet, humans think imperatively, not declaratively and recursion is not always compact nor robust. It is understood that using recursion to solve a problem avoids mutating state; however, it can incur a performance penalty in stack allocation and readability.

Conclusion

"Nothing is more dangerous than an idea when it is the only one you have" ~Emil Chartier Alain

I offered arguments why object-oriented and functional programming styles are evil; however, not the contrary. That is, composition is well suited to the object-oriented style and behavior to the functional style—the two faces of software.

Code isn't good because it's purely one or the other. Code is good when it is easy to reason about—when one can be reasonably confident that it's correct. And if correct code is easily composable into more correct, clear code, then it's great code.

We use the functional style where we can and the object-oriented style where we must—a hybrid style that pays tribute to neither[16]. We seek minimal, composable code that conveys intent with no (unintended) side effects.

Style, in esthetics and convention, contributes to the overall quality of written work. In code, this is more than indentation or syntactical sugar; it is the selection of well-named methods that bind composition and behavior to form elegant solutions to practical problems.

Poor style causes readers to lose interest, good style holds it, and great style is captivating. Apathy for style is infectious and spreads with cruel disregard. Software afflicted with it reads sloppy, disjoint, and counter-intuitive.

In contrast, empathy for style fosters clarity; code says and does no more and no less than promised. Such software is more predictable, testable, and less evil. ??

Links

We invite you to join us in constructive debates on our (LinkedIn) group as we continue a journey through cybernetic principles, patterns, and practices:

Group: https://www.dhirubhai.net/groups/14027931/

Page: https://www.dhirubhai.net/company/gplicity/

Footnotes

[1] A human brain is a prediction machine

[2] Except for initializing value objects (e.g., lists, dictionaries, etc.) A constructor should contain no more than field assignments. More so, a newly constructed object should cause no harm—it should not throw exceptions; it should return default values or empty collections.

[3] As opposed to Alan Kay's original idea that objected-oriented programming is based on communication between biological cells, not things like inheritance, or encapsulation.

[4] The name of a design pattern, class, function, or method conveys a concept

[5] An invariant is a condition that should always hold true

[6] Throwing an exception transfers control outside of the function—violating its guarantee

[7] In so far as implementation (structural) inheritance promotes abstraction; rather, this form of inheritance is more like subclassing

[8] The use of properties to access derived state; such abstractions are reduced to nothing but data structures

[9] Often in separate code files (each containing a single class definition)

[10] Polymorphism is not exclusively object-oriented; it exists in other paradigms but is referred to as such for the referential integrity of the article

[11] the goto still exists, it masquerades as: return, continue, break & yield. It was the backward goto that was banished, not the goto itself.

[12] The if and switch statements; however, sometimes an if is just an if. Like comparisons of primitive objects (<, >, ==, !=), or validating arguments (guard clauses)

[13] Studies have shown that > 90% of bugs are attributed to a single condition expressed in more than one place in the code.

[14] In terms of vertical and horizontal code surface. As a rule-of-thumb, and best practice, don't stray from the left margin too far or too long.

[15] Except for a <summary> section. Caution: over time, even this can harbor lies as well.

[16] We use the object-oriented style to create boundaries and a functional style to operate within those boundaries. Each focuses on a specific type of complexity.

Ivan Valiente

Staff Software Engineer at GRUBBRR

2 年

I love this post, Glenn. Congrats! It's thought provoking and engaging. Maybe even controversial to some, but definitely makes you think. Well written and clearly argued. Keep them coming. ??

要查看或添加评论,请登录

Glenn Puchtel的更多文章

  • I write Dumb/Disposable code

    I write Dumb/Disposable code

    "Every line of code is written without reason, maintained out of weakness, and deleted by chance." Jean-Paul Sartre's…

    1 条评论
  • Cyborg - (bio)Cybernetics Group

    Cyborg - (bio)Cybernetics Group

    To all interested subscribers. If you'd like to engage in an interactive discussion (debate), please join my group:…

  • Periodic Table of Cybernetics

    Periodic Table of Cybernetics

    Good news, bad news. First the bad news.

    1 条评论
  • Software as a Game [SaaG]

    Software as a Game [SaaG]

    Software as a Game [SaaG] A game is a rule-based system with a quantifiable but variable outcome (value). Each outcome…

    1 条评论
  • I'm writing a book

    I'm writing a book

    The August edition of my newsletter on 'oscillation' is (sadly) postponed as I'm writing a book instead. The book…

    1 条评论
  • Bio-Cybernetic Patterns

    Bio-Cybernetic Patterns

    This month's (upcoming) article is on 'oscillation'; however, not till mid-August as I am still writing it. In the…

    4 条评论
  • Temporality

    Temporality

    Cause and effect. Two core pillars of cybernetics.

    1 条评论
  • Biological Models of Reactionary Networks

    Biological Models of Reactionary Networks

    Note: newsletters are progressive; each augments the previous. Reading them in chronological order is suggested for…

    1 条评论
  • Reaction Networks

    Reaction Networks

    Note: newsletters are progressive; each augment the previous. Reading them in chronological order is suggested for…

  • Patterns of Behavior

    Patterns of Behavior

    Note: newsletters are progressive; each augment the previous. Reading them in chronological order is suggested for…

社区洞察

其他会员也浏览了