Me solving programming this morning
Stable, pure-functional, composable abstract interfaces is the key.
At the highest level - proper, “just right” minimal abstractions which “perfectly” (optimally) capture corresponding aspects of reality.
Unnecessary, redundant abstractions is the root of all evil. Especially abstractions as in an Abstract art or abstractions for the sake of abstraction, as in Liberal arts - pure evil idiocy.
Having a pure /expression in place of many possible sequences of imperative commands eliminates a lot of problems. Declarative comprehensions, iterators, futures and pure functional DSLs (made out of functions on ADTs) in general is the way to actually manage complexity.
map
, and fold
were the first functional abstractions, invented to
generalize and encapsulate traversals, which otherwise would include
explicit, error-prone imperative looping. Today Scala 3
has its
immutable collections and related functional DSL (building blocks) as a pure subset.
Our own evolved layered functional DSLs should be at the highest possible level of abstractions (and everything on the same level of abstraction must be at the same level of details).
The behavior of the data “objects” is expressed most naturally in terms of a set of operations that are meaningful for those “objects”. This is the principle behind ADTs, Ocaml’s modules, Haskell type-classes, Rust traits and even most general mathematical abstractions. It is from “modern” mathematics.
A data abstraction (or an Abstract Data Type) consists of a set of objects (a type) and a set of operations (an interface) characterizing the behavior of the “objects”. This is precisely how a Group or a Ring is defined in math.
Non-leaking abstraction barriers (public interfaces of proper ADTs) are partitions that partition the “universe of discourse”, which in our case requires to specify representation and implementation details. These, however, must never be mentioned or talked about at the level of process logic.
Again, MATLAB REPL is all you need to understand (including the enormous complications in implementations of matrices and numerical methods).
In short, as our forefathers emphasized, one has to evolve an actual language in which solutions to the problems will be developed. And, of course, the underlying programming language must be of the LISP/ML family, or Haskell.
GHC (writen in Haskell), Coq (in Ocaml), Agda (in Haskell), Maxima (in Common Lisp), Octave (in C++, yikes) are the hows.
All you need is Lambda:
A programming language should provide all the convenient and required means for extending the language by defining arbitrary high-level functional DSLs and interfaces out of lambdas. (as in Graham’s “On Lisp”).
- everything is an expression which evaluates (reducible) to a value
- everything is a first-class value (literally)
- values are immutable
- immutable bindings of symbols to values (with shadowing).
- these properties imply /referential transparency (or substitution)
And thus one got core mathematics.
- \(\lambda\) (means of abstraction)
- currying (nesting, partial application)
- reduction rules based on the substitution of equal for equal principle
This is Lambda Calculus, enough for everything
- every value has its type (immutable)
Simple-typed Lambda Calculus, to avoid Russel’s paradox
- type-tagged values (strong typing)
- modules (like in Ocaml)
defstruct
(sets of named and typed slots)- Abstract data types (abstractions defined by a set of functions)
And here are LISPs and Schemes.
- built-in support for sum-types (tagged disjoint unions)
- product-types (records)
- pattern-matching on data-constructors (everywhere)
And this is the Standard ML
- unbroken and enforced referential transparency
- type-classes, which are proper set-subset relation on interfaces
as in Haskell
From this basis OOP featured could be build as a DSL (LISPs did this).
OOP is not essential, ADTs, proper modules and Algebraic types are.
Good things to have
- Ocaml’s modules. Just copy and make it a first-class embedded DSL.
- list comprehensions (generalized notation of the Set theory)
- GADTs
IO Monad
(Haskell solved it just right)- monad comprehensions (
for
notation, similar to thedo
notation) deriving (Read, Show)
deriving (FromJSON, ToJSON)
- iterator protocols and syntax in the core language (Traversable)
The Timeless Principles
- Abstraction and formal Specification (all the modern abstract mathematics)
- Proper abstractions hide (abstract out) irrelevant details and representations.
- Write down all specifications for interfaces using Sets and Logic (check with TLA+)
- MATLAB/Octave are ultimate DSLs and the Abstraction principle at work (high-level, while the details are abstracted away and encapsulated)
- Data abstraction - Abstract Data Type of Liskov (only public interfaces)
- Highly modular code - a hierarchy of specialized, reusable modules
- Embrace Change and optimize for changeability (orthogonality of modules, by Norvig).
- Layers of DSLs (CLOS, TeX, Emacs, Racket, the classic Haskell books)
- Mostly-functional. All code should be pure-functions unless it’s impossible (Ocaml).
- Functional /pipelines (graphics, compilers, linkers. GHC, of course)
- Systematic program design of Kiczales
- Haskell with FFI’ing everything (inside a Monad, of course).
- Assertions of preconditions are the best “active commentaries”. Use them.
- Automate everything. Testing, verification, static analysis.
- TTD works for testing of specifications and prototyping. “Laws” and invariants must be specified as tests.
- Read the other people’s code that you use. Same as knowing your food.
- For complex code no choice, but to understand specification and interfaces.