How to program in crappy imperative languages
“Data dominates. If you’ve chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming.” — Rob Pike
This quote summarizes it almost perfectly. There are some of the facts, which it captures.
- The shape of the functions defined by pattern-matching have exactly the shape of the corresponding sum-type type.
- Erlang’s pattern-matching on receive does de-structuring
- selectors of record-types are partial functions.
- in-wire representation of structuted data (Erlang, Clojure).
The fundamental principle is increase the level of abstractions (interfaces) to the highest possible level, while maintaining a layered structure of underlying DSLs.
To put it simple, one has to wrap types and functions over verbose low-level crappy imperative code (everything will be inlined anyway).
The data-abstraction principle and modularity helps.
One has to use high-level features of the language to mimic Algebraic Types and other features of really good languages.
A decent language must support generalized sum-types as a “first class citizens” or even GADTs.
- Rust at least has pattern-matching on Enums.
- Scala has a generalized pattern-matching.
The main problem with the languages of C-family is that low-level imperative constructs are mixed with high-level types (classes) and operations on them. This is exactly how Java, C++ and Rust look like.
But it does not have to be this way. Smart people bootstrap crude DSLs out of high-order functions and use them to “talk” about business logic and a problem domain.
The most important thing is to have clear layers with separate the “levels” - a vertical partitioning of the concepts.
The major “code smell” (or even stink) is when low level imperative stuff of handling memory or concurrency issues is presented in the code that handles business logic. There should be only parts of your DSL, which are appropriate to this particualar level of abstraction.
So, wrap high-level interfaces around imperative crap, design it as a DSL, make a few layers if necessary, and package them into small, specialized modules.
Use Algebraic Types (as Abstract types) as a high-level representations of concepts. Hide the implementation details completely behind abstraction barriers (ADTs).
This is exactly what Scala 3
did to Java - it wrapped better abstractions around
its imperative ugliness and plain stupidity.
“I tend to “grow” programs rather than think them out completely before writing them. This way I don’t tend to make large mistakes before I discover that things have gone wrong. Above all, it’s fun, I get immediate feedback, and I see whether my ideas work as soon as I have typed in the program.” — Joe Armstrong
Formalize with Algebraic types
Sets and a Second Order Logic is enough for formally define everything.
However, Algebraic Types, have an additional structure, besides being sets of arrows.
One should formalize and gradually buuld a prototype with ADTs and pure functions. This is better than pure math.
ADTs and related DSLs is the way. Things like scalatest
is the highest level of
an art. (Programming is still an art, similar to writing a poetry).
High-level, concise DSLs
This is serious. Once one have a proper set of features which are orthogonal to but complement each other (currying, partial application, implicit parameters, traits) one gets the ability do define embedded DSLs as libraries or implement libraries as layers of embedded DSLs.
Just look at what Scalatest
is. It is amazing, wonderful, awesome. This is how
to program.
akka.http.scaladsl.*
is another example.
Sets and Logic
There are evolved formalisms (mental techniques of an external Observer). Neither exists outside ones head.
However, almost everything that can be obveserved can be modeled (of formally defined) with Sets and Second Order Logics.
This fact is the essence of the Curry-Howard isomorphism, which means that proper observations eventyally converge to “What Is”.
It is not a coincedence that we use types (as sets of values) in both the Lambda Calculus and a formal logic (the Narual Deduction system).
Sets partition the universe (of concepts) and this is a fundamental notion, which intelligent observers captured and generalized.
Different kinds of atoms and molecules, from an external observer’s point of view, form a distinct partititons, which he call “classes”.
Common idioms
There are common or even universal idioms in programming. Thet go back to early LISPs and CLU (which was an imperative OO language).
It is important to realize that Abstract Data Types, type-classes (of Haskell) and, well, OO classes are just sets of unterfaces defined in diffrerent ways.
Being, at least conceptually, Sets, they “naturally” have set-subset relations and the union (which is at the essence of everything) and intersection operations.
What Haskell or Scala3 call a type-class are just named sets of interfaces with a set-union operation, defined with a different syntax.
Types as sets of values
Classes, by the way, are just more structured product types - they allow code “slots”, not just data “slots”. CLOS got it right.
Just like any other strucuted (or nested) types, a class is a template for a structured (or nested) value of a particular kind.
Objects differ in that they have not just an internal state (just like records) but also a set of methods to manipulate this state and a self reference (to access it).
In the classic paradigm the there exported functions to manipulate values of a ADT sitting inside a corresponding modules. Referencing thus is explicit.
Data-constructors
There is “universal” operation of producing a new value of a specific type (or a class) - to call a data-constructor, which is a function.
The common and just right syntax is to have them Capitalizes, because this reflects it relations to corresponding mental concepts.
For parameterized types we naturally would have parameterized data-constructors.
Type-Constructors
Type-constructors are always parameterized, because they are type-level “functions” or “instantiations”.
Notice that type-level lambdas are more general concepts and implies “arbitraty” transoframations at the type-level.
Only Scala 3 has them so far. Haskell has type-families which are restricted transformations.
Scala 3
Scala 3
is a crafted language. The first of its own kind. SML/NJ was Haskell were
of the “previous generation” of artistic craftsmanship. Ocaml
is another example.