Self-describing code
It is possible to write code in a manner that allows you to choose whether to execute it immediately or grab a description of what the code does as an Abstract Syntax Tree. All using the same code.
The purpose of grabbing a Syntax Tree might be as a step towards converting the code into a different language, either as a one-off process to migrate to a different platform, as part of the build process for your application, or even at runtime to allow the same underlying code to run in different environments depending on the circumstances.
This behaviour can be implemented with a small library (which I have called `Meta`) and a certain way of working when writing code.
This assumes your code can is composed from the following basic building blocks:
- Type definitions
. Enumerated Types . Object Types . Array Types . Optional Types . Scalar Types
- Function definitions
. Global function definitions . Local (closure) function definitions . Function Inputs
. Value Inputs
. Closure Inputs ("Holes")
. Calls to functions . Assignment to local variables . Returning values . Native function definitions
This covers typesafe functional programming and is enough to make a wide range of applications.
Notice there are no control flow constructs such as `if`, `while`, and `for`. These can all be achieved with functions, as we'll see below.
We will build up a subset of JavaScript which can be considered a domain-specific language (DSL) and which can be used to express a wide variety of applications.
Lets start by imagining that no JavaScript constructs at all are allowed, and then add specific patterns one by one.
Module Namespace Tree
The first thing to add is a way to organise code. You need more than just a single global namespace for most applications.
A simple JavaScript object provides a neat way to create namespaces, putting every type and function that you will define into a hierarchy:
const MyModule = {
MySubModule: {
... contents of submodule ...
},
... other contents of module ...
}
As this is the first construct introduced, there is nothing defined to go inside the modules with the exception of submodules. The set of all allowed programs currently consists of all possible trees of modules and submodules with all possible names, and none of these programs can 'do' anything.
Modules in this form can be easily packaged up as a NodeJS module or can be served to a web browser in exactly the same format and used in the same way. This will intentionally be the case all the way through, to produce so called "isomorphic" JavaScript.
If this format was to be stored in a data structure, it would look like this (these types are created using JavaScript in the format described in this document) :
// This lives in `Code.Meta.Structure` module
Module: (...values) => Meta.Object( Code.Meta.Structure.Module, values, {
name: Meta.String,
subModules: Meta.Array(Code.Meta.Structure.Module)
})
Types
There are various kinds of type which can be composed to describe any arbitrary data, which are all treated as immutable data:
- Scalar Types
. Meta.Integer (whole number) . Meta.Float (number with a decimal point) . Meta.Percent (percentage value) . Meta.String (piece of text) . Meta.Boolean (true or false) . Meta.Binary (arbitrary binary data blob)
- Higher-order Types
. Meta.Array (zero or more values of the same type, in a particular order) . Meta.Bag (zero or more values of the same type, in no particular order) . Meta.Dictionary (zero or more named values of the same type) . Meta.Object (several named values with specific names and types) . Meta.Enum (allows one named value out of a possible set of values)
There are also generic types to consider; it is often necessary for types to have other types as arguments to their definitions.
Object Types
So far, we are only allowed to add modules and submodules, so lets add a new rule. For an object you need to specify what the fields are, and ideally you have a function which can be called to construct an object of that type:
const MyModule = {
// Calling this function constructs a `MemberData`
// eg.
// const member = MyModule.MemberData( 'Bob', true )
MemberData: (...value) => Meta.Object( MyModule.MemberData, value, {
name: Meta.String,
isUpgraded: Meta.Boolean,
})
}
Note we cannot use these object type definitions yet, because we have not defined a way to create or use objects. We just have a way to define a schema for an object in the form of a constructor function.
The definition of the syntax tree node for the object would look like this:
// in module Code.Meta.Structure
Object: (...value) => Meta.Object( Code.Meta.Structure.Object, value, {
name: Meta.String,
fields: Meta.Array(Code.Meta.Structure.Field),
}),
Fields: (...value) => Meta.Object( Code.Meta.Structure.Fields, value, {
name: Meta.String,
type: Meta.Type,
})
Enumerated Types
A Enumerated Type allows for a value to be one of a set of options. It is similar to an object, but unlike an object value which always has all its fields present, an enumerated value only has one of its fields present at any time.
This example defines `MyModule.MemberType`, which can contain either a `MyModule.BasicMemberData` or a `MyModule.ProMemberData`:
const MyModule = {
MemberType: (value) => Meta.Enum( MyModule.MemberType, value, {
basic: MyModule.BasicMemberData,
pro: MyModule.ProMemberData,
})
}
For a simple case where you want one of a set of possible options, but do not need any value to go with the options, then the type of all the options is `Meta.Void`:
const MyModule = {
MemberLoggedIn: (value) => Meta.Enum( MyModule.MemberType, value, {
yes: Meta.Void,
no: Meta.Void,
})
}
The syntax tree node for the enumerated type would look like this:
// in module Code.Meta.Structure
Enum: (...value) => Meta.Object( Code.Meta.Structure.Object, value, {
name: Meta.String,
options: Meta.Array(Code.Meta.Structure.Field),
}),
Notice the options have the same data representation as the object fields, both being a name and a type.