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.