MLud: Language Guide

Warning: This page is not yet complete.

Conceptual Overview

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

Expressions in MLud are made up of several main components:

Delegation

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.

Methods

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:

If 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).

Global objects

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.

Types

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.

Array

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.

Boolean

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.

Character

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.

Closure

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 *)

Integer

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.

List

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.

Map

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.

Real

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.

String

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.

Symbol

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.

Operators

Exceptions

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:

Exception objects can produce a stack trace showing what method calls they propagated through. To view it, use .toString[].

Multiple dispatch

Mlud home page

Other code from Moonflare

Moonflare home

All text and images, but not necessarily linked material, on this page ©1998-2006 Derrick Coetzee and Moonflare and may not be reproduced or used for any purpose without prior written permission except where otherwise indicated.