🧙‍♀️ Quest for the Perfect Abstraction

Oliver Jack Dean

Programming languages are in a continuous quest for the ideal notation, syntax, and abstraction to model computational problems.

There have been many debates on many different themes. From Turing machines, the Halting Problem to algorithmic processing vs computing as a series of transactions and so on.

The quest for modern programming languages revolves around harmonizing diverse programming models with the nuances of end system domain effects.

No language is purely complex or entirely simple; the art lies in finding the right equilibrium. Striking this balance is a big challenge for many.

Complexity and utility are often intertwined. As an analogy - just as one may not fully understand the complexities of electricity but can effortlessly utilize it by plugging in an appliance, the same holds true for programming languages and operating systems. The underlying intricacies might be vast, yet their surface-level utility can be straightforward and universally beneficial.

Anyway, the most sought-after programming languages often combine syntactical elegance, cross-system interoperability, deterministic performance or correctness, as well as other safety guarantees.

While many factors influence a language's popularity, professionals primarily seek "models of abstraction" that reliably assist with specific computational problems at both the process and distributed system levels.

Some languages are targeting process level interactions, others more focused on distributed system interactions whilst other languages, often modern languages - are hybrid in approach.

In another way, programming languages can be thought of as accesible packages of computational primitives and abstractions. These packages surface different combinations of primitives and abstractions, as tools, to be used for modelling problems computationally.

How such programming languages package up different primitives and abstractions as well as how they combine these topics together depends on the language design.

One requirment that often dictates how languages are designed often involves the end computer system domain. In other words, a) how the input of a program is evaluated into some output and b) how the program output interacts with some end system - creating system effects.

There is a bunch of other design considerations here but you get the idea.

According to Wikipedia, end computer systems or domains can be sorted into three main classes:

  • (1) transformational system domains that take some inputs, process them, deliver their outputs, and terminate their execution; a typical example is a Compiler;
  • (2) interactive system domains that interact continuously with their environment, at their own speed; a typical example is the Web or a Web App;
  • (3) reactive system domains that interact continuously with their environment, at a speed imposed by the environment; a typical example is the Automatic Flight Control System.

Each system comes with different design constraints and naturally, to design for different constraints requires different combinations of primitives.

Primitives can be conceptualized as mechanisms or foundational language features.

So, primitives serve as packaged abstractions designed for modeling computations and consistently evaluating these computations within end system domains.

Primitives encompass a range of topics, including:

  • Concurrency
  • Composability
  • Resource safety
  • Type safety
  • Error handling
  • Asynchronicity
  • Observability
  • ... and more.
  • How programming languages compose and merge primitives depends on what end system domain the language needs to interact with.

    Some designers may also be creating Domain-Specific-Languages (DSL) - focusing on a narrow end system domain or particular kind of problem.

    BUT at the end of the day, nearly every programming language uses primitives to help with two important things:

    1. Evaluation: Think of this as a big regular expression designed to capture both semantics and syntax of program logic via rules and primitives within a program.
    2. Causality: How such expressions of program logic is stepwise evaluated through a system using time-based events (i.e., system clocks or continuous time models).

    The relationship between evaluation and causality comes in different shapes and sizes - depending on the programming model or paradigm. There are many models/paradigms:

      Clocks/Causality Usage Evaluation Usage
    Logic Uses a backtracking mechanism to explore possible solutions, ensuring causality in inferences. Evaluates statements based on logical rules and facts, deriving conclusions.
    Constraint Constraints ensure a causal relationship between variables, maintaining a consistent state. Evaluates by finding values that satisfy all constraints.
    Array Operations inherently maintain order and causality across array elements. Evaluates operations over entire arrays or matrices, often in parallel.
    Stack-Based Stack operations ensure a last-in, first-out causality. Evaluates by manipulating the top elements of the stack with operations.
    Flow-Based Data flows through predefined paths, ensuring a causal order of processing. Evaluates by processing data as it flows through nodes or components.
    Dataflow Data-driven execution ensures causality as functions execute when data is available. Evaluates functions as soon as their input data is ready, enabling parallelism.
    Aspect-Oriented Aspects can be woven into the main logic, ensuring causality in cross-cutting concerns. Evaluates by integrating aspects at specific join points in the main logic.
    Reflective Reflective actions maintain causality by allowing programs to modify themselves. Evaluates by introspection and self-modification, often at runtime.
    Collection-Oriented Operations on collections maintain order and causality across elements. Evaluates operations over entire collections, often leveraging parallelism.
    Functional Pure functions ensure no side effects, maintaining a clear causality in operations. Evaluates expressions based on function application and composition.
    Synchronous Reactive Logical clocks order events, ensuring synchronous and causal reactions to stimuli. Evaluates reactions in a single, uninterrupted step in response to events.
    Procedural/Imperative Sequential steps ensure a clear causality in operations. Evaluates by executing instructions step-by-step in the order they appear.
    Object-Oriented Method calls on objects maintain causality in encapsulated state changes. Evaluates by invoking methods on objects, manipulating their internal state.
    Asynchronous Callbacks/promises ensure causality in handling results after asynchronous operations. Evaluates by initiating operations and then handling results once they’re available, often outside the main flow.
    Concurrent Uses synchronization primitives to ensure ordered and safe access to shared resources. Evaluates tasks concurrently, leveraging multi-threading or parallelism, while managing shared state and resources.
    Scripting/Dynamic Uses runtime interpretation, allowing for dynamic execution flow and adaptability. Evaluates scripts at runtime, often with dynamic typing and on-the-fly code modifications.
    Probabilistic Uses probabilistic constructs to model uncertainty, ensuring causality in probabilistic events. Evaluates by sampling or inference algorithms to compute distributions over possible outcomes.

    Each one of these programming models contain sub-languages. Some programming models may share the same language.

    For example, say Haskell for functional programming and Python for Scripting or Interpretive programming.

    When such languages are executed, the written programs create and derive different end system effects. These are computational effects of computer programs, also known as side effects - are how the outputs of a programs interacts with an end system domain.

    Different end system domains also have different effects:

      Transformational System Effects Interactive System Effects Reactive/React-Based System Effects
    Logic Derives conclusions through deterministic logical inference. Adjusts logical derivations based on dynamic predicates. Reacts to dynamic rule sets, updating inferences accordingly.
    Constraint Solves for consistent variable assignments within constraint boundaries. Dynamically refines solutions as constraints evolve. Reactively maintains solution spaces upon constraint fluctuations.
    Array Performs bulk operations on homogeneous data structures. Adapts array computations based on dynamic data inputs. Reacts to data shifts, recalculating array operations in real-time.
    Stack-Based Processes data using LIFO semantics with stack operations. Modifies stack content based on interactive commands. Reacts to data inputs, adjusting stack operations on-the-fly.
    Flow-Based Channels data through predefined processing nodes. Modifies data pathways interactively based on user-defined logic. Reacts to data streams, dynamically rerouting or processing data.
    Dataflow Triggers computations as data dependencies are satisfied. Spawns computations in response to dynamic data inflow. Reacts to data availability, synchronously initiating dependent tasks.
    Aspect-Oriented Weaves cross-cutting concerns into primary logic during compile-time. Dynamically invokes aspects in response to runtime events. Reacts to system triggers, executing associated aspect logic.
    Reflective Modifies program structures via introspective mechanisms. Adapts program behavior through runtime introspection. Reacts to system states, dynamically altering its own operations.
    Collection-Oriented Processes bulk data with high-level operations. Adapts collection operations based on dynamic user inputs. Reacts to collection changes, recalculating operations in real-time.
    Functional Transforms immutable data through pure function evaluations (first-class functions). Interactively computes results based on user-defined functional inputs. Reacts to input changes, ensuring stateless and deterministic computations.
    Synchronous Reactive Transforms events into synchronous outputs in a single step. Interactively processes events, ensuring synchronous outputs. Reacts to external stimuli, producing deterministic and ordered outputs.
    Procedural/Imperative Sequentially alters system state through ordered instructions. Interactively modifies system state based on user-driven commands. Reacts to external changes, adjusting the sequence of operations accordingly.
    Object-Oriented Transforms object states via encapsulated methods. Interactively alters object states through method invocations. Reacts to environmental stimuli, invoking object behaviors to maintain state consistency.
    Asynchronous Initiates tasks and processes results post-wait. Handles results post-asynchronous operations, decoupling from main flow. Reacts to task completions, ensuring non-blocking continuation of operations.
    Probabilistic Computes distributions or outcomes from probabilistic models. Adapts probabilistic models based on dynamic observations. Reacts to new data, updating probabilistic beliefs and distributions.

    Modern programming languages, including academic ones, are evolving into hybrids making use of different ways to fuse primitives and abstractions from various programming models or paradigms together - it's a "best of multiple worlds" approach.

    Sometimes, such hybrids are known as "Big Idea" languages too. So, taking features from different programming models (paradigms) and fusing or blending them together to help combat different end system effects like Kotlin or Mozart/Oz.

    Same goes for languages like Zio which blends different programming models together but for Scala end system domains.