In this post, I draft a declarative language using a simple application as an example. Using an example helps me think about usability while drafting an artefact.
The target language is meant to provide enough power of expression to put together applications quickly using existing component libraries. Several ideas contribute to the design of this draft.
- Allow functions – functions are useful to ensure that the language is powerful enough.
- Avoid explicit control flow – control flow can easily make code hard to read.
- State orientation – behaviour is modeled as discrete state transitions.
- Separation between parsers and execution models – this may be essential to allow efficient integration of existing libraries and frameworks (I will discuss this somewhere else).
I use XML as a starting point, but I assume that the result may be translated into source for a language such as Java, C++ or Python. I dropped angled or curly brackets because I want a concise notation, so at first glance the resulting source looks like Python code.
Sample application
The sample application is a text editor. In spirit, this is a very cut down version of a couple of IDEs I’ve written before; an important requirement is that our text editor needs to be extensible.
Source code
The application uses the following files:
control
application.s
model
model.s
data_manager.s
TextDataService.s
view
view.s
view_manager.s
TextViewService.s
deployment
setup.s
Analysing the sample application
control -application.s
The control layer is implemented as a list of commands. In this source file, I establish a few conventions:
- The source file determines a runtime instance (not a class) [1]. That it is an instance is given by starting with a lower case letter [2]. This means that the [application] object is meant to be instantiated immediately when the application starts – in the same way that an html tag specifies an instance of the matching html entity.
- Since the existence of the file implies a container, we need not re-state such container within the file; as a result all commands are top level.
- Imports are denoted by & [3]. This is wholly equivalent to an import statement in Java, but I do not want to reserve words.
- Actions are functions that cannot return a value; actions are denoted by > [4]
- Annotations are denoted by @ and precede the annotated element [5]
I have implemented only one command.
>open
view_manager.display(content,view.desktop)
document: data_manager.forPath(path)
path: openFile()
Notes about the above:
- Statements are listed in reverse order. This will seem counter-intuitive, but allows concentrating on the goal action first [6].
- The : colon notation is equivalent to = in Java [7].
I am not totally satisfied with the above – I would like to emphasize the fact that open() aims at adding a document and a view for this document. This would be simple (and also more declarative). I’ll come back to this later.
model – model.s
The model defines only the *forPath query required by application.s:
*forPath(path)
data[path]
#data[path]-: data_manager.forPath(path)
Additional notations are introduced here:
- The star symbol (*) denotes a query [8]. This allows a function to return a value.
- Query functions are evaluated in return scope [9]. Return scope is unboxed upon returning (here,
data[path]is returned) - Like actions, queries are evaluated bottom up.
- # defines an exception to the declarative query structure[10]. This means that the declaration prefixed by # is evaluated against the object scope, not the return scope. If we didn’t prefix using #, my guess is that an interpreter should define data and data[path] in return scope. But what we really want is define
data[path]asdata_manager.forPath(path). - The notation -: indicates weak assignment [11]. Weak assignment implies that the right hand is evaluated and assigned only if the left hand is null or undefined.
data_manager.s manages data services:
*forPath(path,data)
-services.*.load(path)
-services.*.forPath(path,data.*)
This forPath query introduces two new notations:
- The dash symbol (-) , denotes weak assignment to return scope. This is not completely new and is meant to be consistent with the -: notation
- Used within a declarative statement, the star symbol (*) refers to any element [12]. In this case,
services.*.load(path)indicates thatload(path)is evaluated against
all elements in services.
TextDataService.s is the only data service that I have implemented. This introduces no new notations except maybe the fact that *forPath(path,data) returns null by default.
view – view.s
View is the most simple object. This specifies a component hierarchy along with a shortcut to the desktop component. Note that most components are anonymous.
view_manager.s view manager redirects the display action to all available view services.
TextViewService.s implements the display(document,desktop) method required by view. The + sign indicates adding an anonymous element [13].
deployment – setup.s
This links view and data services for text using injection [14].