(Meta) language, Part 1

What is this?

This article proposes a simple modeling language with a few syntactic and semantic tricks that allow using the language itself as a metalanguage. The language would be used to generate a persistent data model and several queries over that data model (both to read and to write). The underlying system for representing the data is the entity-property-binding architecture from this article.

Definitions and queries

The system defines entities, properties, bindings and queries. Queries, in turn, will involve subsets (filtering entity instances based on predicates involving their properties and bindings) and operations performed to all instances in these subsets.

Consider a simple data model for a library: an author has a name, a book has an author, a title and a publication date. Subsets which we would like to manipulate (beyond singletons) are the list of books of an author, the list of books with a certain title, the books published between two dates, and the authors with a certain name. Using the appropriate syntax:

entity author
property name[author] : string

entity book
property title[book] : string
property publication[book] : date

binding written_by[book : many book ; author : one author] 

def authorByName n =
  author |? name[_] = n
end

def booksByTitle t =
  book |? title[_] = n
end

def booksBetweenDates f l =
  book |? publication[_] > f |? publication[_] < l
end 

def booksByAuthor a =
  book |? written_by[book : _ ; author : a]
end

The syntax of defining entities, properties and bindings is pretty straightforward : between square brackets, the involved entities are mentioned with the appropriate multiplicity (for bindings) or value type (for properties). Subsets are a little bit more complex: they involve a simplified functional notation.

Every entity, when used in a subset expression, represents the set of all instances of that entity. This set is then filtered using the |? operator : list |? predicate is equivalent to the Objective Caml List.filter predicate list. This involves simplified functional notatioin for making anonymous function definition shorter. Here, name[_] = n is equivalent to (fun x -> name[x] = n). To determine which expressions belong to the anonymous function and which don’t, the rule implies only syntactic context. That is, the anonymous function starts as the placeholder argument _ and grows until it reaches a context where a function would be expected (such as the function side of the operator |? or any other functional operator).

Other functional operators:

x | f is equivalent to f x

x |: f is equivalent to List.map f x

x |! f is equivalent to List.iter f x ; x

exists x: f is equivalent to List.exists f x

forall x: f is equivalent to List.forall f x

In terms of the language, sets of entities are filtered and manipulated this way. The fundamental piece of behavior in this language is therefore chaining a value through several mutators.

A few example of queries (reading the titles and publication dates of books by an author, deleting an author, changing a book title and adding a new author):

def titlesOfBooksByAuthor a =
  booksByAuthor a |: title[_], publication[_]
end

def deleteAuthor a =
  booksByAuthor a |! delete ;
  delete a
end

def setBookTitle t b =
  title[b] <- t
end

def addNewAuthor n =
  new author | name[_] <- n
end

This introduces three new constructs: delete merely deletes its argument (an entity instance) from the data model and returns unit. property[instance] <- value sets the property of the selected instance to the specified value, then returns the instance (to allow chaining (as in the example of the add-new-author query which returns the author after setting its name). new entity creates a fresh instance of the entity, for which all properties must be set before they are read. Constraint violations (binding multiplicity, the fact that properties are used before being initialized) are signaled as runtime errors and there are attempts to detect them at compile time. Queries are transactional, meaning that an exception or runtime error rolls back the entire transaction.

Last but not least, let us introduce modules to handle all the namespacing issues, using the same convention that modules start with a capital letter and non-modules don’t (this is only a convention, not a lexical constraint). The chosen syntax is:

def Library =
module
  entity author
  property name[$.author] : string

  entity book
  property title[$.book] : string
  property publication[$.book] : date

  binding written_by[book : many $.book ; author : one $.author] 

  (* ... *)

end 

By default, the $ variable represents the current module (so I could create a function which takes an argument that has the same name as a module member, and use name to represent the parameter and $.name to represent the module member). Of course, this variable is optional, which means that when a variable name cannot be resolved, a ‘$.‘ is appended to see if it exists as a module member (should this fail, an error is launched.

Metaprogramming

In the current state, all objects are first-class citizens, including modules. This means that functions can be used to manipulate entities, properties, queries, modules, and so on. For instance, assuming that testToken determines if an authorization token is valid:

def addRightsControl func token object =
  if ! testToken token object then throw AccessDenied ;
  func object
end

def safeSetBookTitle title token book =
  addRightsControl (setBookTitle title) token book
end 

An interesting addition here is the pattern keyword. First, let’s consider its usage and semantics, and let’s then examine why it makes sense within the language semantics:

pattern UriIdentified decoratedEntity =

   (* To indicate violation of the uniqueness rule *)
  exception UriNotUnique : string

  (* Associate an URI to every instance of the entity. *)
  property uri[decoratedEntity] : string

  (* Access an element by its URI. *)
  def byUri uri =
    decoratedEntity |? $.uri[_] = uri
  end

  (* Overload operator 'uri[object] <- uri' to enforce uniqueness. *)
  def write($.uri) val object =
    if ! empty ($.getByUri val) then throw ($.UriNotUnique val) ;
    $.uri[object] <- val
  end
end

def Page =
module
  (* Define basic properties of a page *)
  entity page
  property content[$.page] : string

  (* Associate pages to Unique Resource Identifiers *)
  usepattern UriIdentified $.page

  (* Provide an URI-to-content shortcut. *)
  def getContentByUri uri =
    $.getByUri uri |: $.content[_]
  end
end

This example creates a pattern : a means of extending the behavior of a module by adding new entities, properties, bindings and other various definitions to that module. The pattern in the example introduces a new exception, a new property (bound to a specified entity that was passed as an argument), a shortcut for accessing the entity (if any) with an identifier and an overloaded version of the write-operator for the property. This pattern can then be used as part of any module using the usepattern statement.

Simple things for simple people

The language now has a variety of keywords with seemingly distinct functionality. This is not actually the case: all the functionality above can be explained using the same rules revolving around chaining through a sequence of mutators.This is because, fundamentally, statements such as entity, property, usepattern, binding, exception or def are just that: mutators.The rules for these mutators are:

The expression… …returns the original module with an additional…
m entity e
entity
m def name = expr end
field with the expression bound to the name
m property p[...] : …
property
m binding b[...]
binding
m exception ex : …
exception
The expression…
…returns…
module
an empty module (with no members).
m pattern name args = defs end
same as m def name args x = x defs end
m usepattern f same as f m

In particular, a pattern is nothing more than a function which is applied to a module and returns a module, and usepattern is just a synonym for the pipe operator | (except that the lambda argument _ is replaced by $ to the right of an usepattern statement). In general, to the right of any of the keywords above save pattern, the dollar symbol represents the module being modified.

So, in essence, adding a function to a module is not any more complex than:

def addSomeMethod mod f =
  mod def method = f end
end 

(addSomeMethod module { x -> x + 1 }).method 10 (* Outputs 11 *)

An additional detail about definitions is that they overwrite previous definitions with the same name (this is also true for built-in operations, such as writing to a property). However, it’s also possible to later remove a definition, which restores the last available definition.This allows defining private elements in modules, but also in patterns which don’t want to overwrite existing definitions with the same name in other modules.

def example =
module
  def frobnicate x = x + 1 end
  def one x = $.frobnicate x end (* Returns x + 1 *)

  def frobnicate x = x / 2 end
  def two x = $.frobnicate x end (* Returns x / 2 *)
  hide frobnicate

  def three x = $.frobnicate x end (* Returns x + 1 *)
end

Other ideas

This is just a basic overview. The next articles in the series will deal with:

  • Using names as first-class variables (manipulating names, concatenating names, generating fresh names locally).
  • Some example patterns that can be easily accomplished with the system.
  • Reflection facilities for exploring a module (in addition to merely adding things to it).
  • A type / predicate system for checking and for reflection.
  • Deciding between runtime metaprogramming and compile-time metaprogramming.

0 Responses to “(Meta) language, Part 1”


  1. No Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>



659 feed subscribers
(readers who polled a feed this week)