User Tools

Site Tools


at:tutorial:metaprogramming

Metaprogramming

AmbientTalk without syntactic sugar

In AmbientTalk, everything is an object, also native values such as numbers, booleans, strings and tables. Hence, the only way to interact with these values is by sending messages to them. However, there are some constructs in AmbientTalk that do not seem to send a message at all. Examples are table access/assignment, control structures and arithmic operations:

>[5,6,7][2]
>>6
>if: 1 == 2 then: { 13 } else: { 42 }
>>42
>1 + 2 * 3
>>7

However, these constructs are all syntactic sugar that enable a more natural syntax for performing these operations. Behind the scenes, they all perform message sends. The following code shows the equivalents of the previous code, but with the actual message sends:

>[5, 6, 7].at(2)
>>6
>(1 == 2).ifTrue: { 13 } ifFalse: { 42 }
>>42
>1.+(2.*(3))
>>7

In the first example above the message at is sent to the table [5,6,7]. The second example sends the keyword message ifTrue:ifFalse: to the boolean value returned after evaluating the expression (1 == 2). Notice the usage of the curly braces. These wrap the expressions in a literal closure object. A literal closure is just an object with a special apply method that will execute the body of the literal closure. The usage of these closures ensures lazy evaluation of the actual arguments of ifTrue:ifFalse: similar to the use of blocks in Smalltalk or Self. Finally, in the third example, mathematical expressions are converted to method invocations on numbers.

Quasiquoting and splicing

Quasiquoting and splicing are an advanced and powerful metaprogramming techniques that control the evaluation process and allow one to manipulate the evaluation process of the abstract syntax tree.

Quoting

Any valid AmbientTalk expression can be quoted. This prevents the expression from being evaluated. Instead, it is returned literally.

Quoting an expression is done with the ` operator. There are four variants:

  • `… for quoting literal values
  • `(…) for quoting expressions
  • `{…} for quoting statement lists
  • `[…] for quoting tables
Literal values

Quoting a literal value such as a number or a string results in the exact same number or string.

>`3 == 3
>>true
>`3 + 8
>>11
>`"text" == "text"
>>true

An identifer can also be quoted. This returns a symbol object:

>foo
Undefined variable access: foo
>`foo
>>foo
>`foo.text
>>"foo"

Note that in `foo.text, the quoted expression is foo and not foo.text. The selection of the field text is performed on the result of the quotation, in this case a symbol.

Expressions

To quote a complete expression, it has to be wrapped in parentheses:

>`(foo.text)
>>foo.text
>`(foo(1, 2, 3))
>>foo(1, 2, 3)
>`(1+2)
>>1.+(2)
>`(o.m())
>>o.m()
Statements

Statements (definitions, assignments, …) can also be quoted, but only inside a quoted statement list. Trying to quote a statement in the same way as an expression will cause a parse error. Instead it is necessary to wrap the statements in a closure using ``{ and ``}.

>`{ def a := 4 }
>>def a := 4
>`{ def tab[5] { m() }; tab[3] := n() }
>>def tab[5] { m()}; tab[3] := n()
Quoting a literal closure can be done in two ways: as an expression:
`({ |a, b| a + b})

or as a statement list:

`{ |a, b| a + b}

To quote a literal closure with the latter form, the vertical bars may not be ommited, even if the closure takes no arguments.

`{ || a + b }

yield a literal closure expression, while

`{ a + b}

yields a statement list.

Tables

Literal tables can already be defined as follows:

>def tab := [ 1+2, 3+4, 5+6 ]
>>[3, 7, 11]

With this construct, all the elements of the literal table are evaluated. By quoting a literal table, all the elements are quoted instead of evaluated:

>def anotherTab := `[ 1+2, 3+4, 5+6 ]
>>[1.+(2), 3.+(4), 5.+(6)]

Unquoting

Inside a quotation, an expression can be unquoted as well. An unquotation escapes from the quotation and causes the unquoted expression to be evaluated. The return value is then used as the quotation of the unquotation. Unquoting an expression is done with the # operator.

>def msg() { `foo }
>><closure:msg>
>def arg(n) { n+5 }
>><closure:arg>
>`(o.#(msg())(#(arg(1))))
>>o.foo(6)

Splicing

Splicing can already be used without quoting:

>def upTo(n) { def idx := 0; def tab[n] { idx := idx + 1 } }
>><closure:upTo>
>[ 7, 8, 9, @upTo(4) ]
>>[7, 8, 9, 1, 2, 3, 4]
>[ 7, 8, 9, upTo(4) ]
>>[7, 8, 9, [1, 2, 3, 4]]

In the example above the elements of the table returned by invoking upTo(4) are added in place to the table in which the expression was spliced. Evaluating the same expression without the splice operator adds the table rather than the elements of the table. Hence, the use of the splice operator removes a level of nesting and adds the elements in place to the table.

Splicing can also be used in combination with quoting and unquoting. AmbientTalk provides the unquote-splice operator #@ that can be used to splice the value of an unquotation into a quoted expression.

>`(o.m(a, b, #@(upTo(5))))
>>o.m(a, b, 1, 2, 3, 4, 5)
>`[ @upTo(3), #@(upTo(2)), #(upTo(3))]
>>[@upTo(3), 1, 2, [1, 2, 3]]

First-class abstract grammar

Quoting an AmbientTalk expression results in an the parse tree of that expression. Like any value in AmbientTalk, parse trees are objects that respond to messages. This means that the abstract grammer of AmbientTalk is first-class: programs can manipulate and create asbtract grammar elements.

>`(f(1, 2, 3)).function
>>f
>`(f(1, 2, 3)).arguments
>>[1, 2, 3]

Any abstract grammar element can serve as a prototype for a new one:

>def application := `(f(1, 2, 3))
>>f(1, 2, 3)
>application.new(`g, [4, 5, 6])
>>g(4, 5, 6)

The following example uses meta-programming and reflection to generate a proxy for an object that provides a given interface. The interface's method list is examined and for each method specified in the interface, a method is generated that delegated to the actual object.

TODO: Update code
def isMethodDefinition: statement { true };

def policyOf: object with: interface {
	
	def policyDefinition := interface.method.bodyExpression.statements;
	
	policyDefinition.each: { | statement |
		if: (isMethodDefinition: statement) then: {
			def methodBody := statement.bodyExpression.statements;
			if: ((methodBody.length == 1).and: { methodBody[1] == `nil }) then: {
				def bodyExpression := `{ #(object) ^ #(statement.selector) ( #@(statement.arguments) ) };
				statement.bodyExpression := bodyExpression;
			}
		}
	};
	
	object: interface;
	
};

Read / Eval / Print

AmbientTalk reifies the read, eval and print operations. This means that you can read any string and get the responding syntax tree for it, evaluate any syntax tree and get a value for it, and print any value and get a string representation of the value.

>read: "1+2"
>>1.+(2)
>def result := eval: `(1+2) in: self
>>3
>print: result
>>"3"

Eval is a keyworded message that takes another parameter, namely the object in whose scope the expression must be evaluated.

>def o := object: { def x := 4 }
>><obj:{super,super:=,x,x:=}>
>eval: `x in: o
>>4

Multi-stage (Generative) Programming

Here's a small example of “compile-time” metaprogramming or “multi-stage programming” inspired by the same example from the E language on Multi-stage programming in E:

Below is a regular power function. Given two numbers 'x' and 'n', it returns 'x^n':

def pow(x, n) {
  if: (n == 0) then: {
    1
  } else: {
    x * pow(x,n-1);
  }
};

Let's see whether it works:

system.println(pow(2,5)); // prints 32

Now, consider the following 'expandPow' function that is very similar to the above function, but which, instead of calculating the value of the power function will build an expression that, when evaluated, yields the value of the power function. Note that this function is parameterized with the name of the variable that is used to calculate the power value:

def expandPow(var, n) {
  if: (n == 0) then: {
    `1 // `exp returns an abstract syntax tree for exp
  } else: {
    // within a quoted expression, #(exp) evaluates exp,
    // expects it to return an AST, and embeds that AST
    // in the quoted expression
    `(#var * #(expandPow(var, n-1)));
  }
};
 
// this prints the expression 'y * y * y * 1'
system.println("expandPow(`y,3) = " + expandPow(`y,3));

To be able to use the expression generated by the above function, let's define a small helper function that will embed this expression in a first-class function:

def powMaker(n) {
  def ast := expandPow(`x, n);
  // the built-in function 'eval:in:' takes an expression and an object and
  // evaluates the expression in the scope of the given object
  // return a function that has the expanded expression as its body:
  eval: `({|x| #ast}) in: self;
};

Now we can generate power functions that are fixed in their second argument, but that are more efficient to execute:

// pow5 is now bound to a function { |x| x*x*x*x*x*1 }
def pow5 := powMaker(5);
 
system.println(pow5(2)); // yields 32, as expected

You can measure the performance difference by timing the evaluation of both functions:

import /.at.support.timer
system.println("pow(2,5) takes " + (time: { pow(2,5) }) +"ms");
system.println("pow5(2) takes " + (time: { pow5(2) }) +"ms");

The quoting and unquoting mechanism of AmbientTalk makes it really easy to “partially evaluate” a function in one of its arguments.

The above example is available in the AmbientTalk library, under '/at/demo/metaprogramming.at'.

at/tutorial/metaprogramming.txt · Last modified: 2009/11/21 07:44 by tvcutsem