Random Thoughts on Computer Languages

Elsewhere I can be found:
icychkn@protonmail.com
github/icychkn
youtube/icychkn

Application Development is Language Development

Building an application with any language involves turning the language into something that resembles the desired application.

Take the stereotypical hello world program for learning how to define functions:

function hello(): print("Hello, World!") hello()

The function "hello" does not exist in the base language, yet it can be referenced as if it were because defining "hello" extends the language.

What Is A Program?

A program is a list of instructions that affect the state of the machine executing them, which in turn affect the effects of succeeding instructions.

   machine
    V
... A B C D ...

When machine executes instruction B, it applies B to the state of the machine resulting from executing instructions "... A". Once B is finished, the machine may execute C which in turn depends on the effect of instructions "... A B", before moving on to "D ..." ad infinitum.

This is essentially the Turing machine, minus the state table. Felt a bit silly when I realized that.

Circular Self Hosted Systems

An interesting issue arises in self hosted systems developed from within themselves. Any version of the system is dependent on a chain of prior versions. The circular loop unravels into a neat dependency chain:

bootstrap -> N' -> N''
             A  <-  V

bootstrap -> old N' -> old N'' -> ... -> N' -> N''

I'm not a fan of this, it makes it difficult to rebuild the system with another bootstrap if the source depends on some high level feature implemented within itself existing in it. It also makes it difficult to revert features and/or replace them.

For an example, let's say N' implements feature X and N'' uses it to bootstrap feature Y and replace it. Eventually I realize feature Z is the better of X and Y and wish to replace Y with it. But while X would make implementation of Z trivial, its near impossible with only Y. Now I must reimplement X before Z can be added if I didn't save N'.

A solution to this problem is to have the system only depend on some subset of itself existing and using it to bootstrap higher level features, maintaining the links between bootstrap and full-featured.

Semantics:
  | ... | := ... features available
  A -> B  := A implements B
Example:  | X | -> | Y | -> | Y X | -> | Z |
Solution: | X | -> | X Y | -> | X Z |

Y can now be easily reverted and replaced with Z without the need to preserve older versions. Although, this does require an unusual language to permit bootstrapping certain high level features, especially syntax-dependent ones.

Functions are Classes

Function definitions are classes and their applications are objects.

When you apply a function, it instantiates an "object" for its "class" and executes the constructor for it.

(lambda () 5)

This defines a class that returns an object 5.

(lambda (x) x)

This defines a class that simply returns the object passed to it. Each instance of this class has an isolated value of x.

But now we come across an area that modern OOP languages fall short compared to FP.

(lambda (x)
  (lambda (y)
    (+ x y)))

This defines a class that, when an instance is made, creates and returns a class that returns an instance of a number.

Most static, typed, OOP languages do not permit creating classes at runtime. And a pattern exists, attempting to work around this restriction, known as the Abstract Factory pattern.

This makes me curious how OOP could be improved to at least match this power of FP. Or, better yet, if a system could be built based solely on this.

First Class DSL Support

General-purpose languages are like one-size-fits-all shirts: Fits well enough for the job, yet will never match the comfort of one tailored specifically for your needs and sizes.

Domain-specific languages are designed so the underlying language disappears leaving only the domain to work in. Unfortunately, in hiding the host language, they strip its power as well; leaving only a limited abstraction for the domain.

A better approach is to allow escaping the domain temporarily to use the host language. Much like how Python's Jinja templates work, allowing data to be passed in to build the final document.

Even better would be to allow DSLs to be accessed from within another. If you have one dedicated for mathematical calculations, there's no reason to not allow using it from a template language for expressing a complex equation in a natural manner.

To build off of a previous post: "What Is A Program?", an instruction may affect the interpretation of future instructions, allowing for a sort of DSL system to be developed naturally.

Everything is an Interpreter

A major feature used in the comparison of programming languages is whether it is interpreted or compiled.

While it may have been easier to divide languages by this design choice before, nowadays with the proliferation of Just-In-Time compilers embedded into interpreters and compiled languages embedding scripting engines for runtime execution, the line has thinned considerably with much overlap.

My view is everything in the computer is interpreted, it is merely a matter of at what level. Classic interpeters build an AST and walk through it executing corresponding instructions. Virtual machines often represent programs as bytecode for efficiency. And the CPU interprets instructions in memory using circuits made up of transistors.

In this regard, classic compilers merely reduce a program's representation from a flexible AST to a simpler, and consequently more efficient to execute, linear form targeting some particular machine, hardware or virtual.

The Fundamental Flaw with OOP

When I was learning OOP, a particular summary of it appeared frequently: "attaching functionality to the data they operate on".

OOP allows a single name to represent different operations dependent on the object's implementation of it, making use of the object's private namespace to avoid collisions with the same operation specialized for other objects.

Quite a useful concept, but what if you want to use the same name for different implementations dependent on context?

For one example, let's say I want to print numbers as roman numerals. Where does this functionality go? In classic OOP, we'd move it into the numbers class. But what name do we give it? To avoid colliding with the normal print method, we can call it "PrintNumeral". Not a big deal, the new method may prove useful enough to outweigh the cost of including it in the numbers class.

Let's move on to another example, a compiler. Let's say I have a set of classes implementing AST nodes and I wish to implement compiling for both Windows and Linux. Where does this functionality go?

If I implement both in each class, they could be either CompileLinux and CompileWindows or a generic Compile method that uses an argument to choose the target. This works well until you make changes to how compiling works, then you have to go through each and every class and update them.

An additional possibility, that is popular in Java, is to move the compiler to its own class, having both Linux and Windows methods that test the type of AST nodes before dispatching to the proper functionality. This system is easier to change when needed, but it brings back the original issue OOP was meant to avoid: operations are separate from the data they operate on.

OOP was designed to attach operations to data in a globally accessible form, but makes no attempt to allow context specific implementations. All methods in a class are accessible to every instance of that class.

As another summary of OOP goes: You wanted a banana, but you got the banana, the gorilla holding the banana, and the entire jungle.

This problem reminds me of an older project known as GZigZag. It was based around connecting data to other data through "dimensions". While 3 is after 2 and before 4, in the sequence of odd numbers it is after 1 and before 5. It allowed the same object 3 to have meaning in both the sequence of natural numbers and odd numbers. Both dimensions were traversable from the same cell.

Context should be independent of the data itself, yet possible to access using the data as a reference.