This is an old revision of the document!
Table of Contents
Appendix
In the appendix, we explain useful libraries available to the AmbientTalk/2 programmer. These libraries provide abstractions ranging from traditional, established “collections” up to newly researched language constructs, such as “ambient references”.
Unit Testing Framework
The file at/unit/test.at
shipped with the AmbientTalk/2 system library defines a unit testing framework for AmbientTalk/2 which is similar in spirit and structure to JUnit and SUnit. Load the module by executing import /.at.unit.test
.
Creating a Unit Test
To create your own unit test, make an extension of the UnitTest
object which is exported by the unit testing module. In the extension, define zero-arity methods starting with the prefix test
. Here is an example:
def myUnitTest := extend: UnitTest.new("my unit test") with: { def testSomething() { self.assertEquals(3,1+2); } }
You can run a unit test by sending to your unit test object the message runTest()
, for example:
myUnitTest.runTest()
This will execute all test*
methods in the given unit test (in an undefined order!), and print out which of the tests succeeded or failed. The runTest
method can optionally take a “reporter” object as an argument, which can be used to implement a custom strategy for reporting success or failure of a unit test. The default reporter object is a text-based UI.
Like in JUnit and SUnit, it is possible to define two methods named setUp()
and tearDown()
that are invoked in between each individual test*
method. Never rely on the lexical order of your unit test methods for the purposes of initialization, etc.! Unit test methods may be exacuted in an arbitrary order.
Assertions
Within a test*
method, you can use a number of assertion methods to assert certain properties of your code:
assertTrue(boolean) assertFalse(boolean) assertEquals(o1,o2) assertNotEquals(o1,o2) assertLessThan(o1,o2) assertGreaterThan(o1,o2) assertLessThanOrEquals(o1,o2) assertGreaterThanOrEquals(o1,o2) assertMatches(str, pattern) assertNotNil(val)
Each of these methods also takes as an optional last parameter a reason, which is a text string describing what the assertion checks. This string is printed when the assertion fails and can be used to provide more understandable error messages.
Finally, two more useful auxiliary methods exist:
assert: exceptionType raisedIn: closure fail(reason)
The assert:raisedIn:
method executes the given closure
and checks whether this leads to an exception of type exceptionType
. If so, the exception is caught and further ignored. If no exception (or one of the wrong type) is raised, the assertion will fail. The fail
method can be used to explicitly make a unit test fail with a given reason.
assertEquals(…)
). However, these methods are not lexically visible, rather they are defined in the UnitTest
parent object. Hence, the proper way to invoke them is via a self-send, as shown in the above example.
Asynchronous Unit Tests
Up to now, the unit testing framework assumed that all of your unit tests consisted of purely synchronous method invocations. When running the tests, all test*
methods are invoked sequentially, and the unit test ends when the last test*
method has been invoked.
To support unit test methods that need to perform asynchronous invocations (e.g. those performing concurrent or distributed unit tests), the unit testing framework introduces a new prefix: all methods that spawn asynchronous computation must be prefixed with testAsync
.
When a method is prefixed with testAsync
, the unit testing framework expects the method to return a future and will only process subsequent test methods once that future is resolved or ruined. Here is an example that tests whether future-type messaging works:
def testAsyncFutures() { import /.at.lang.futures; def adder := object: { def inc(x) { x+1 } }; def f := when: adder<-inc(42)@FutureMessage becomes: { |val| self.assertEquals(43, val); }; f }
The unit test framework will invoke this method, receive a future f
, and only continue once the future f
has been resolved. In the above example, the future f
is the return value of the when:becomes:
function, which means that f
implicitly depends on the future associated with the call to ←inc(42)
.
It is also possible to use makeFuture()
to create a fresh future explicitly within the unit test method, and to use the returned resolver to resolve the future at the appropriate time.
Test Suites
It is possible to group multiple unit test objects into what is known as a “test suite”. Running the test suite runs all of the component unit tests. You can create a new test suite as follows:
def myTestSuite := TestSuite.new("my test suite", [unittest1, unittest2, ... ]);
The TestSuite
object groups the given unit test objects. You can execute all tests in batch by sending the test suite object the runTest
message, just like for running a single unit test. It is possible to nest multiple test suites within each other.
Basic Collections
The modules /.at.collections.vector
and /.at.collections.list
define a Vector and List datastructure respectively.
Vector
A vector is a dynamically resizable AmbientTalk table (aka array). Indexed reading from and writing to a vector is fast (O(1)). Adding elements to a vector is mostly fast, but sometimes requires a resize of the vector. Vectors support the traditional stack operations push
and pop
and may be turned into sets by invoking their uniq
method (note that a uniq
-ed vector is not permanently a Set: subsequent duplicates added to the vector will not be filtered).
Vectors may be created as follows:
import /.at.collections.vector; def v := Vector.new(10); // a vector with starting length 10
The constructor optionally takes a comparator as a second argument. A comparator is a binary function returning a boolean whose job is to compare elements of the Vector. This comparator is used among others when sorting the vector. The Vector's interface is as follows:
// returns the number of elements in the vector (not its capacity!) length() // returns whether the vector contains elements or not isEmpty() // is the vector at max. capacity? atMaxCapacity() // return idx'th element or raise an IndexOutOfBounds exception at(idx) // write idx'th element or raise IndexOutOfBounds exception atPut(idx, val) // iterate over the vector each: iterator, returns nil // map a unary function over the vector, returns a new vector map: fun // accumulate a function with a given starting value inject: init into: accum; // returns a new vector whose elements satisfy "cond" filter: cond; // implode a vector of character strings into one text string implode() // join a vector of character strings together with the given string join(txt) // returns a range [start,stop[ as a table select(start, stop) // appends an element to the back of the vector. Returns the vector itself add(element) // alias for add(element) <<(element) // insert an element at a given position, causing subsequent elements to shift one pos to the right. Returns this vector insert(atPos, element) // delete the element at the given position, shifts all following elements one pos to the left. Returns the value of the element at the deleted position. delete(atPos) // adds elements to the back of the vector push(element) // deletes elements from the back of the vector pop() // return the index of the first element matching the unary predicate or nil if none is found find: filter // remove the given element from the vector, return true if the element was actually found and deleted, false otherwise remove(elt, cmp := defaultComparator) // remove all objects for which filter(elt) returns true removeAll: filter // destructively appends otherVector to self. Returns this vector addAll(otherVector) // empties the vector clear() // Return a new vector whose elements form the set-union of all elements in self U otherVector union(otherVector, cmp := defaultComparator) // Return a new vector whose elements form the set-intersection of all elements in self ^ otherVector intersection(otherVector, cmp := defaultComparator) // Return a new vector whose elements form the set-difference of self \ otherVector difference(otherVector, cmp := defaultComparator) // Quicksort the vector in-place. The comparator defines the ordering among elements. sort(cmp := { |e1,e2| e1 < e2 }) // Turn the vector into a set without duplicates in O(nlogn + n) // The vector's ordering is lost (it becomes sorted) uniq(cmp := defaultComparator, ordercmp := {|e1,e2| e1 < e2 }) // return an element drawn randomly using a uniform distribution from the array or raise an EmptyVector exception. random() // return a table containing all elements of the vector asTable()
The file at/collections/vector.at
contains a unit tests that further helps to illustrate the usage of this Vector abstraction.
List
The module /.at.collections.list
implements Scheme-like list datastructures. The module exports the prototype NIL
, which is bound to the empty list. Non-empty lists are defined as a chain of cons-cells.
The list module defines two styles to manipulate cons-cells: an object-oriented and a functional style. The object-oriented style represents cons-cells as Cons
prototypes. Given a cons-cell c
, a new one can be constructed by invoking c.new(car, cdr)
. The car and cdr part of the cons-cell can be extracted by means of c.car
and c.cdr
.
The functional style allows one to manipulate lists by means of the following functions:
cons(car,cdr) -> a new cons-cell car(conscell) -> the car cdr(conscell) -> the cdr list(@items) -> a cons-cell representing the head of a list
Lists (cons-cells or the empty list) support the following operations:
// accessors for car and cdr car() cdr() // the length of the list length() // whether the list is empty or not isEmpty() // returns the nth element of the list nth(n) // apply a unary function to each element of the list each: fun // apply a function to each element and its index in the list // i.e. list.eachWithIndex: { |elt, idx| ... } eachWithIndex: fun // map a unary function over the list, returning a new list map: fun // accumulate a value over a list inject: init into: accum // return a new list whose elements satisfy the unary predicate filter: cond // does the list contain the element? contains(elt, cmp := DEFAULTCOMPARATOR) // implode or join a list of text strings implode() join(txt) // drop the first n elements from the list tail(n) // prepend an element to the list add(elt) // insert an element in the list (functionally) insert(atPos, element) // return a new list where the element atPos is deleted delete(atPos) // functional append append(aList) // return the index of the first matching element, or nil if none is found find: filter // return the index in the list of the element or nil of not found indexOf(elt, cmp := DEFAULTCOMPARATOR) // return a list where the given element is removed remove(elt, cmp := DEFAULTCOMPARATOR) // return a new list where all objects for which filter(elt) is true are removed removeAll: filter // convert the list into a table asTable()
The file at/collections/list.at
contains a unit test that further illustrates the usage of the list datastructure.
Top-level functions
The file at/init/init.at
shipped with the AmbientTalk/2 system library contains the code that is evaluated on startup within every actor created in the system. Because the definitions are evaluated in every actor's top-level scope, these definitions will be globally visible in every file. Below, we describe the standard functionality provided by AmbientTalk/2's default init
file.
Asynchronous control structures
The init
file defines a number of useful control structures that operate asynchronously.
loop:
defines an infinite asynchronous loop. That is, the block closure is executed, then asynchronously applied again:
loop: { ... }
An if
-test on a future for a boolean:
whenTrue: booleanFuture then: { ... } else: { ... }
Asynchronous while loop over future-type conditional:
asLongAs: { /* asynchronous computation returning a future */ } do: { ... }
Mobile code
The function script:carrying:
can be used to define a “pass-by-copy” closure, as follows:
def mobileAdder(x) { script: { |n| x + n } carrying: [`x] }
A call to mobileAdder(5)
returns a closure which, when applied to a number, returns that number incremented with 5. Unlike regular closures, which are pass-by-far-reference when passing them to another actor, the above closure is pass-by-copy. The result is that a remote actor can apply the closure synchronously. The catch is that for this to work, the closure must specifically list all of its lexically free variables in the carrying:
parameter. These variables will be copied along with the closure when it is parameter-passed.
The constructor function isolate:passAs:
allows you to define an isolate object with a custom serialization strategy. For example,
def foo := 42; def i := isolate: { ... } passAs: { |foo| /.some.Object.new(foo); }
The above code defines an isolate object i
which, when passed between actors, becomes a some.Object
on the other side. Note that state (foo
in the example) can be transferred as usual via the parameter list of the closure.
Custom Exceptions
The module /.at.exceptions
defines a number of auxiliary methods which can be used to define one's own custom exceptions. Here is how to define a custom exception FooException
. First, define a new type tag with which clients of your code can catch the exception:
deftype FooException;
Next, define a prototype exception object using the createException
function exported by the exception module. As a convention, an exception prototype object is prefixed with X
:
def XFooException := createException(FooException);
XFooException
is now bound to an object which is tagged with the given type tag, and which implements two methods: stackTrace
, which returns an AmbientTalk stack trace for the exception, and message
, which returns a string indicating what went wrong. The object also has a constructor taking a new message as an argument. You can now raise your custom exception as follows:
raise: XFooException.new("reason for what went wrong");
If your custom exception requires additional state, you can define it as an extension of the prototype exception. If you define a custom constructor, do not forget to initialise the parent object, as follows:
deftype IndexOutOfBounds; def XIndexOutOfBounds := createException(IndexOutOfBounds) with: { def min; def max; def idx; def init(min, max, idx) { super^init("Index out of bounds: given " + idx + " allowed: [" + min + "," + max + "]"); self.min := min; self.max := max; self.idx := idx; }; }
The exception module also exports an auxiliary function error(msg)
which can be used to raise a “quick and dirty” runtime exception with a given message. It also exports the prototypes of a number of standard exceptions that can be raised by the language runtime itself.
Language Extensions
The files in the at/lang
directory define custom language features which mostly use AmbientTalk/2's reflective facilities to extend the language.
Futures and Multifutures
Futures
The module /.at.lang.futures
provides support for futures. Futures have already been described as part of the concurrency section in the tutorial.
The module exports the type tags OnewayMessage
, FutureMessage
and Due
:
- Tagging an asynchronous message with
FutureMessage
will attach a future to the message. - Tagging a message with
OnewayMessage
ensures no future will ever be attached to the message. - Tagging a message with
@Due(timeout)
associates a future with the message that is automatically ruined with aTimeoutException
after the giventimeout
period (in milliseconds) has elapsed.
Messages can be automatically associated with a future by invoking the enableFutures()
function, which enables futures for all messages, except those tagged as a OnewayMessage
.
The futures module also exports the function when:becomes:
to await the resolution of a future, and auxiliary when:becomes:catch:using:
functions.
Futures can also be created and resolved manually:
import /.at.lang.futures; def [fut, res] := makeFuture(); when: someAsynchronousComputation() becomes: { |value| res.resolve(value); // resolve the future manually } fut // return the future to a client
Finally, the futures module also provides some auxiliary functions, of which group:
is often a very useful one. The group:
construct groups a table of futures into a single future which is resolved with a table of values or ruined with an exception:
when: (group: [ a<-m(), b<-n() ]) becomes: { |values| def [aResult, bResult] := values; ... }
Multifutures
The module /.at.lang.multifutures
provides support for multifutures.