2025-12-04

software simplicity

external form

  • variable names must only contain english words

    • use shortened english word names if they are unambiguous in context (e.g. count instead of element_count)
    • exception: single-letter variable names are acceptable and even preferred if structure dominates over data in very small scopes.

      • example: (map (lambda (a) (+ 1 a))). especially when the same mapping function is used in multiple contexts, then it may not matter what "a" is, only what happens to it.
      • if single-letter variable names are used, they should be i or j for indices, or otherwise assigned in alphabetical order, a, b, c, etc, instead of arbitrary initials. then at least the letters carry some order information instead of full word meaning that is harder to recover by the reader and more easily leads to conflicts
  • avoid bindings that are only used once
  • avoid identifiers with uppercase characters
  • no operator overloading
  • keep code to the essentials
  • minimal examples that run
  • minimal examples that use all features

conceptual organization

  • strict call by contract

    • everything that can be handled by the call contract should be handled by the call contract
    • always assume valid input
    • no asserts and defensive checks outside the contract
    • simplify by extending the contract, not by adding code
    • defensive checks are wasteful when their conditions are excluded by design
    • separating input validation allows the core logic to execute faster and remain clearer, providing a valuable reduction of complexity
  • structure modules to match visible or logical boundaries
  • keep the call hierarchy tidy
  • use bottom-up accretive composition
  • software development is managing complexity
  • each component should do one thing well. much complexity in software comes from one thing trying to do two things
  • "do not repeat yourself". reduce multiple sources of truth and duplication
  • "you are not going to need it". do not implement functionality or scaffolding until there is a demonstrated, current requirement. reduce speculative complexity and unused structures
  • algorithmic identity encoding: language-independent method description
  • purely functional terms can be seen a canonical encoding that can be projected upwards into imperative, object-oriented, or state-based implementations, and not equivalently reversed.

    • avoids hidden state change and the resulting order dependence
  • convention over configuration. sensible defaults to derive from avoid repeated boilerplate re-configuration
  • principle of least surprise. reduce unexpected behavior or behavior that is hard to predict
  • avoid hidden control flow or registries. the hidden state changes make it harder to reason about and it causes order dependency and hinders clean abstraction
  • intermediate formats and defined mappings between them
  • prefer positive conditions. if (a) {b} else {c} over if (!a) {c} else {b}'
  • use language truth semantics consistently: avoid x == 0 and simply use ! in c
  • inline when small and used at most twice; outline when large or used more than twice
  • it is possible to omit error handling for instructive purposes
  • no pseudo access control such as public/protected/private or static/extern tags for bindings
  • backward compatibility does not have to be considered for new programs
  • referential transparency
  • identifiers replaceable by their values versus variables that are associated with contextual state
  • idempotency
  • falsifiability
  • reduce coupling
  • key-value tree structures
  • derivative configuration files
  • common conventions that are not part of the language are often not worth following. keep it simple

testing

tests can be split into two categories: basic and detailed.

basic tests test minimal calls and compositions. compile errors, type signature changes, raised exceptions, and segfaults become immediately obvious. after changes, there tend to be many unknowns about the required adjustment effort. it is helpful to know early that the code still runs on this basic level. intricate changes in the content of the calculations dont affect a lot of code of these tests, which makes them easier to maintain. it is easier to make this layer of tests work after changes, before focusing on more specific input and assertions. assertions are only added to the data flow, never adding to or otherwise influencing the rest of the basic test code.

detailed tests test finer assumptions about the processed data. these tests build on a systems where the basics already work. not mixing it in with the basic tests keeps things simple and separable.

this separation is also more suitable for automated refactoring, which can focus on the detailed test schemes and use examples from the updated minimal call api.

c-specific

  • single-compilation units

    • include headers and even c files directly when libraries are small
    • eliminates linker indirection and improves optimization visibility
  • declare and define all variables at the beginning of each function before usage

    • matches c's actual stack allocation behavior. the idea "as late as possible, as early as necessary" is clever but c does not allocate like this anyway.
    • gives an overview of storage use within the function
    • separates initialization from mixed declaration and initialization inside the body. it is just simpler.
  • use implicit type casts where possible: avoid unnecessary explicit casts unless required for correctness
  • no usage of -- or ++; use explicit arithmetic instead
  • do not rely on syntax that depends on indentation
  • do not use enums; their typing and auto-numbering do not justify the overhead
  • use 0 instead of NULL, as the macro increases complexity without adding any expressive power beyond 0
  • use pointer arithmetic instead of taking the address of array index access
  • avoid unused initializations or deinitializations
  • define the targeted platform and standards. for example: c17, posix.1-2008, lp64, little endian, libc. platform: x86_64 linux, musl
  • use inttypes.h
  • while(1) instead of for(;;)
  • prefer caller-provided memory (allows stack or heap choice, and argument bulk allocation)

scheme-specific

other

suckless software written with simplicity in mind