Warning: This page is not yet complete.
Languages such as Java, C++, or Smalltalk make a distinction between classes, which are abstract descriptions of objects, and instances, which are concrete objects with data. In MLud, as in other prototype-based languages, this distinction does not exist; instead, there are only concrete objects, and new objects are formed by "cloning" existing ones. The cloned object will initially act like its parent in every way, but then may be modified to fit a new situation by modifying its data or adding, removing, or overriding methods. Thus clone serves the purposes of both instantiation and subclassing, with the added benefit of the parent object providing "default" data values.
As in Java and Smalltalk, the MLud object system is rooted, meaning
the single object $root is an ancestor of all other
objects. However, $root is not special, and this rule
can be broken, but shouldn't be.
Expressions in MLud are made up of several main components:
12345 for the integer 12345 or [1,2] for the
array containing 1 and 2. See the Types section for more.+,
>, or, and the indexing operator []
work exactly as you expect. All boolean operators are spelled out as
words. Note that := is used for assignment, and
= for comparison. See the operators section for
details.(* comment here *), as in ML.blah. Local variables are
declared using:
new varname := <initial value>;
If the initial value is omitted, void is assumed.
.) followed
by a name, this is an access to a data member, or slot, of the current
object this. Slots are added using new .slotName;.?: operator, in that the
statement as a whole evaluates to the value of the last statement in
whichever branch executed. For example, this returns 1 if a number
x is greater than 0 or -1 otherwise:
return if x > 0 then 1 else -1;
new i := 0;
while i<10 do i := i+1;
for(i := 0; i<10; i := i+1)
$console.print[i.toString[]];
While loops check the given condition on entry. If it's true, they execute the statement, and check the condition again, repeating this until it becomes false. For loops have three parts, an initializer, an exit condition, and an increment. The initializer is executed on entry, and then the condition is checked. If it is true, the body followed by the increment are repeatedly executed until the condition becomes false.
<Object expression>.<Method name> <Arguments (array expression)>
blah.foo[2]. Here, we are invoking
the method foo on the object referenced by the local variable
blah with a single argument 2. If the object is omitted, the
current object this is assumed. However, in general each of the
parts above can be generated by a more complex expression, such as in:
(if x>0 then 5-x else x-5).(funcSymbolTable[i])(argArray + [3,4]);
When the function name used is not a simple word, it must refer to a symbol, and it must be enclosed in parenthesis.
If an object doesn't know how to handle a method call that is
invoked on it, the first thing it does is to ask all of its parent
objects if they know how to handle it. This asking is called
delegation, because the object delegates its duties to other
objects, and it is what allows clones to inherit all the behaviours of
their parent objects. In MLud, delegation is purely dynamic, meaning
an object can change its parent objects at any time. Effectively, this
powerful mechanism, which is invoked through the
.setDelegates method, allows an object to change its
type. Be very cautious with this method, as it can form loops, or
disconnect an object from $root.
MLud is largely a procedural language, like C or Java. Each method consists of a sequence of statements, terminated by semicolons. In fact, this rule is held even more strictly in MLud: even blocks, such as in if statements or for loops, must have a semicolon following their terminating brace, such as in this example:
if not .location.isVoid[] then {
.location.removeContent[this];
};
.location := void;
A full method requires a prototype at the top which establishes a
required parent object for each parameter. If omitted, the parent is
assumed to be $root. For example, if you want a method
which takes a descendent of your object $room, an integer,
and one more arbitrary parameter, you might write it like this:
foo[aRoom : $room, n : $integer, other] {
(* do stuff with parameters here *)
}
To add this method to an object blah at runtime, we would do this:
blah.setMethod["foo", "foo[aRoom : $room, n : $integer, other] {\n" +
" (* do stuff with parameters here *)\n" +
"}"];
A number of special variables are always available inside a method:
this: indicates the object the method was invoked uponcaller: this in the method this method was invoked fromdefiner: the object this method is defined on; because of delegation, this may be an ancestor of thisargs: an array of all the argumentsIf you want a function to accept an arbitrary list of parameters,
you can use the @-notation to accomplish this, but note
that this sidesteps type-checking:
max[i : $integer, @moreIntegers] {
(* Here moreIntegers is an array of parameters two and onwards *)
}
The value of the last statement evaluated in a method will be its
return value. If returning from a method in multiple places, use the
return statement, as in return 0;. Don't use
it where it isn't needed though; it bears a small runtime cost.
You can declare two methods with the same name, as long as
the parameter types are different (this includes the type of the
object the method is defined on). Sometimes, more than one method
will match the types of the given parameters. For example, we might
define another function foo with weaker type requirements:
foo[aThing, n : $integer, other] {
(* do stuff with parameters here *)
}
In this case a sophisticated mechanism called multiple dispatch is used to decide which method should be invoked. In general, it will try to choose the one with the strictest type requirements for every argument (the type which descends from all other available types).
Although most objects are accessed through other objects, some
built-in objects are more convenient to access directly. We call these
global objects. To access such objects we use the
$ (dollar sign). For example, the root object is
typically accessed as $root, and the scheduler object is
accessed as $scheduler. To create a new global object,
use this syntax outside of a function in your MLud file:
.newGlobal['name_of_global, <initial value>];
This is useful for creating objects which act as abstract classes.
You can later create instances and/or subclasses by invoking
$global_name.clone[].
Implementation note: Each global object defines an
accessor on $root. When you use $ notation,
in reality this invokes a function on the current object, which
delegates to $root, which returns the "global"
object. Thus no data is ever really stored outside the objects. This
is part of why an object not delegating to $root breaks
things.
MLud includes a number of optimized built-in types designed to make many everyday tasks easier. They vary in their capabilities and tradeoffs; a good MLud user chooses the right type for the job.
Note that, unlike most constructs in MLud, values of built-in
data types (such as 3) are not objects. However, they can be viewed
in many ways as an object with a single fixed parent object. For
example, 3.toString[] works, even though 3 can't handle
it, because it causes $integer.toString to be called
with this set to 3. This gives you the ability to
customize handling of built-in types in many useful ways by adding
methods to the parent objects.
For more information about the specific functionalities of
each type, see the comments in the source code for each type
parent object in src/mlud/types. This information
will eventually be added to the page.
Arrays, with parent object $array, are mutable
sequences of fixed size. They always form the argument list
in a function call. Literal arrays are written as comma-separated
lists of values with brackets around them, as in:
[1,$root,2,"fred"]
It's quick to access a particular location in an array, but it's slow to insert or remove an element, slow to concatenate two arrays, and slow to copy an array. If you do these operations a lot, or you need persistence, you should consider using a list instead.
A boolean variable, with parent object $boolean, is
always equal to one of the global variables $true or
$false. The result of most comparison operators like
< is a boolean. To indicate true or false,
use the global variables.
A character, with parent object $char, is a single
textual symbol, such as the letter a. As in C or Java, these
are written with single quotes around them, as in 'a'.
The same escape codes are supported; for example, '\n'
indicates a newline.
A closure, with parent object $closure, is like
a Smalltalk block, or a closure in Lisp. A literal closure is
written:
<arg1,arg2,arg3> {
(* Do things with args *)
}
Even when there are no arguments to the closure, an empty
<> must precede it. Closures can access variables
in the context in which they are formed, even after that context
is gone, for example:
getAdderClosure[i : $integer] {
{ i+j; };
}
We could use this function like this:
new fiveAdder := .getAdderClosure[5]; fiveAdder[7]; (* returns 12 *)
Integers, with parent object $integer,
hold arbitrary-size integers such as 5, 0, or
-1200000000000000000000000, and literal integers are
written this way. All the usual arithmetic operations,
including ^ for powers, work on integers.
Lists are sophisticated data structures which allow adding or
removing from both ends, as well as concatenation of lists with
+, in amortized constant time. In addition, lists, like
maps, are persistent, meaning that no operation actually
changes a list; instead, it produces a new list that shares
storage with the old list. This makes it possible to safely pass
someone a list without worrying that they'll modify it. Literal lists
are written:
list(1, "hello", $root, 2)
Because concatenation takes constant time, you could do something like this, which constructs a list of size 210000 (about 103010):
new lst := list(1);
new i;
for (i:=1; i<=10000; i:=i+1)
lst := lst+lst;
This seems like it should fail, since your computer doesn't have 103010 bytes (or 103001 gigabytes) of memory. In reality it takes only a relatively small amount of memory, because large portions of the list share storage.
Maps, with parent object $map, are associative arrays,
also called hashes. They allow you to associate key objects with value
objects and later to query for the value object associated with a
particular key object. This is useful in a variety of applications.
Literal maps are written:
map(1=>"hello", $root=>2)
This map maps 1 to "hello" and
$root to 2, as is visually apparent.
To use a map, simply index into it using the brackets ([])
operator:
toyMap.insert["Sara", $doll]; toyMap["Sara"]; (* Gives $doll *)
Maps, like lists, are persistent, meaning that no operation actually modifies a map; instead you get a new map that shares storage with the old one. This allows you to pass a map to another object without fear of them modifying it.
A real, with parent object $real, is a
double-precision floating-point number. Reals support
a smaller range than integers, but are faster to use
than large integers and can approximate any real number.
A string, with parent object $symbol represents a
sequence of characters, and are used to hold words, sentences, and
so on. Literal strings are, as in C and Java, written with double-quotes
around them:
"Bob the Fisherman\n"
Strings can contain escape codes, as in C, which represent single
characters that cannot be typed inside a string, such as \n
for a newline.
Symbols, with parent object $symbol, are useful as
identifiers, much like an enum in C. A literal symbol is written
as in Lisp with a preceding quote ('), as in
'bob. Symbols can be quickly compared for equality,
but unlike strings cannot be mutated, although they can be
transformed into a string.
Exceptions work much as in C++ or Java. Exceptions
should all delegate to $exception, and are thrown
using throw and caught using try-catch
blocks. Note that a try-catch block, like an if-then
block, must be followed by a semicolon:
try {
.blah[2];
}
catch $methodNotFound with {
(* do stuff *)
};
The syntax of catch is somewhat unusual. It takes an
object, normally a global but conceivably any expression, and catches
any object with that object as an ancestor. It then invokes the given
closure on the thrown object. If the closure is not a literal closure,
such as a variable containing one, it must be enclosed in
parentheses. If you wish to catch all exceptions, catch
$exception. To rethrow an exception, simply throw the
object again.
Some exceptions are produced by the runtime system rather than MLud code. These include:
$argumentArrayNotArray: produced when the argument
array passed to a function is not an array object. Be sure you put
[] around the argument to a one-argument function.$methodNotFound: attempted to
execute a method which does not exist, either because the argument
types are wrong, of the wrong number, or the name is wrong.$slotNotFound: attempted to access a slot on
an object which does not exist.$objectTypeError: indicates an object of the wrong
type was given to a system component by a means which does not
do type-checking.Exception objects can produce a stack trace showing what method
calls they propagated through. To view it, use .toString[].