
Scripting is perhaps the most important (though not the most difficult) part of the
project I have conceived. In order for everything to work, I need a general-purpose language, with variables, conditional execution, loops, and exceptions. I don't need something complicated, like
anonymous functions or
closures . Most likely, even
recursion will not come in handy to me, in any case, as long as no application was found for it, in any of my
cases . In this language there will be no
syntactic sugar at all, since all the tasks of metaprogramming will be
taken over by
XSLT . In general, this language will be as simple as possible, but ... not simpler.
Let me remind you that the script, in my understanding, contains not only the definition of a number of functions defining the "behavior" of the objects participating in the game, but, in addition, must define the structure of quite complex objects, such as the board, the figures, etc. P. Here is
an example of such a description:
Definition of '' board ''(board (grid (dimensions "A_J" "1") (direction (name forward) 1 0) (direction (name backward) -1 0) ) (grid (dimensions "A_J" "2") (direction (name forward) -1 0) (direction (name backward) 1 0) ) (grid (dimensions "A_K" "3") (direction (name forward) 1 0) (direction (name backward) -1 0) ) (grid (dimensions "a_d")) (link (name forward) (J1 J2) (A2 A3) (K3 K3)) (link (name backward) (J2 J1) (A3 A2) (K3 K3)) (zone (name rebirth) (positions F2)) (zone (name protect) (positions F3 G3 H3 I3 J3)) (zone (name beauty) (positions F3)) (zone (name water) (positions G3)) (zone (name truth) (positions H3)) (zone (name ra) (positions I3)) (zone (name stop) (positions F3 K3)) (zone (name end) (positions K3)) (zone (name dices) (positions abcd)) )
This definition is a kind of “map” of a board game and can be very complex. It was for the analysis of such hierarchical structures that I needed
XPath , which, in turn, led to the need to use
XML for the internal representation of the script in memory. Analysis of such structures is
verbose , but rather straightforward.
Code is another matter.
(define (factorial na) (if (<= n 1) a else (factorial (- n 1) (* an)) ) ) (define (factorial n) (factorial n 1) ) (define main (set! out (factorial 5)) )
I hope you recognized him. Yes, the
factorial value is calculated here, using the “tail recursion”, and no, I am not going to engage in the optimization of the “tail recursion”. As I said, I hardly need recursion. For those who suddenly do not know what “tail recursion” is and why it may need to be optimized, I recommend reading
this wonderful book .
')
This code is just a compact test, covering as far as possible all the functionality that interests me at the moment. It uses the conditional operator, calls and function definitions,
overloading and working with parameters. Since I / O operators are not provided in the language (they are not required), in the test, the output will be emulated by assigning the total value of the “variable”
out .
By the way speakingThe complexity of the selected test is quite justified. Calculating the factorial of the three, I noticed that the resulting value “2” is somewhat different from the expected one. An investigation of this situation led to the following
fix . I could well not have noticed this error using a simpler test.
It can be noted that all control constructs of a language are functions, in the sense that they always return a certain
value . For example, the result of
an expression
sequence is the result of the last expression executed. We should also mention the
exceptions :
(define dice-drop (check (in-zone? dices)) (check is-empty?) drop-pieces add-move )
At any time, a validation of some Boolean expression can be performed. In the event that the resulting value is false, execution must be immediately interrupted. I do not know a tool that is more suitable for this task than exceptions.
CheckExpression computes the value of its argument in a boolean context and "throws" a
CheckException if the condition is violated (it is obvious that this function will always return the true value, unless it comes down to it before returning the value).
Values of the function arguments will be calculated
strictly , before the control is passed to the functions. There will be no total
laziness of calculations, but functions such as
if ,
and or
or will calculate the values of their arguments only when necessary. To call arbitrary functions (defined in the script body), the
ApplyExpression construction is
intended . The resulting argument values are nothing else but local variables whose scope is limited to the function being called. All variables are accessed via the
IEnvironment interface, passed to each calculated expression:
public interface IEnvironment { ... void letValue(String name, IValue value) throws EvaluationException; void setValue(String name, IValue value) throws EvaluationException; IValue getValue(String name, boolean isQuoted) throws ValueNotFoundException; ... }
Using this interface, you can create local variables (using the
let function), change their value (using the
set! Function), and also get the value by name. For the
latter of these operations, a special function is not provided. Any
atom that is not a string or numeric literal will be treated as a reference to a variable (or a function parameter), provided that a function with the same name and
arity has not been defined in the script.
Details for the curiousCollisions between function calls and variable calls could be avoided by always framing function calls with parentheses, as follows:
(define dice-drop ... (drop-pieces) (add-move) )
I found this record too cumbersome. In addition, there was a problem with the
internal representation of such structures in XML. Another unrealized feature is the calculation of the name of the function being called:
((if (< ab) "+" else "-") ab)
In any case, I do not think that the use of such structures is necessary.
In order to simplify the language as much as possible, I did not explicitly implement the
QUOTE operator (therefore, it is unlikely that I could write
Quine in this language). There is a citation in the language, but it is implicit. For example, such expressions as
let and
set! “Know” that the first argument should not be calculated (the overridden
IExpression.isQuoted method is responsible for this). Thus, the first argument, in these expressions, is perceived as the name of a variable, and not an attempt to obtain its value.
More detailsIn the application code, there may be more complex situations associated with implicit quoting:
(define (piece-move direction) (check up) (check direction) ... ) ... (moves (piece-move north) (piece-move south) (piece-move east) (piece-move west) ... )
Such code will occur very often. Here, the atoms
north ,
south ,
east and
west should be interpreted as
names of directions, but only under the condition that such directions on the board are defined (otherwise, it is a reference to variables). Thus, the
direction parameter will contain a string with the name of the corresponding direction.
Positioning and navigation are performed by referring to pseudo-variables with appropriate names. Calling on the “variable”
up (provided that such a direction is determined on the board) will move the marker of the current position (as a side effect), and also return
true if this movement was completed successfully.
But what happens when you call the
direction parameter? If no additional action is taken, a non-empty string value will be received, which, in a boolean context, will be interpreted as
true . This is clearly not what we would like. To remedy the situation, the module that provides access to the calculation of the course of motion must check if the resulting string is a name of a position or a direction. If so, the
get operation must be re-executed and its result must already be returned to the dial peer. This is a good illustration of the thesis that simplifying a language can lead to a complication of its implementation.
Taking into account all the above, code generation becomes quite
trivial . It is required only to find the definitions of functions in XML and recursively construct hierarchies of the expressions corresponding to them.
Expressions themselves are created by
ExpressionFactory , which allows you to easily expand the list of supported system functions (up to the implementation of new language constructs).
Of course, this is not the end of the road. In order for such scripting to be useful, it is necessary to associate it with navigation in the game space and a change in the course calculation state. Need to determine the mass of system functions that work in terms of the subject area, such as
is-friend? or
is-empty? . Finally, to implement a move generator, you need to realize the possibility of
non-deterministic calculations . Compared to all these things, factorial is really easy.