Alice ML

advertisement
Alice ML
Alice ML is an extension of the Standard ML (SML). Standard ML is a type-safe programming
language that embodies many innovative ideas in programming language design. It is a statically
typed language, with an extensible type system. It supports polymorphic type inference, which
all but eliminates the burden of specifying types of variables and greatly facilitates code re-use. It
provides efficient automatic storage management for data structures and functions. It encourages
functional (effect-free) programming where appropriate, but allows imperative (effect-ful)
programming where necessary. It facilitates programming with recursive and symbolic data
structures by supporting the definition of functions by pattern matching. It features an extensible
exception mechanism for handling error conditions and effecting non-local transfers of control.
It provides a richly expressive and flexible module system for structuring large programs,
including mechanisms for enforcing abstraction, imposing hierarchical structure, and building
generic modules. It is portable across platforms and implementations because it has a precise
definition.
It provides a portable standard basis library that defines a rich collection of commonly-used
types and routines.
________
Interactive toplevel ________________________________________
Alice ML is an extension of Standard ML, and the Alice interactive toplevel works very similar
to the interactive prompts known from other SML systems, where you can type in expressions
and declarations to evaluate them. Input is terminated with a semicolon. For example, you
might simply perform a simple calculation:
- 4+5;
val it : int = 9
The expression is evaluated and the result 9 printed along with its inferred type int. Anonymous
expressions get the name it, so that you can refer to them in consecutive inputs:
- 2*it;
val it : int = 18
We can also try the one-line Hello World program:
- print "Hello world!\n";
Hello world!
val it : unit = ()
Entering a function declaration is more interesting. For example, the factorial function:
fun fac 0 = 1
| fac n = n * fac (n-1)
val fac : int -> int = _fn
This time, the result has a more complex type: it is a function from integers to integers. We can
apply that function:
fac 12
val it : int = 479001600
If a computation takes too long you can abort it in the interactive toplevel by entering Ctrl-C. For
example:
fun loop () = loop ()
val loop : unit -> '1 = _fn
loop ()
<Ctrl-C>
interrupted
Note: In the graphical interface to the toplevel you have to interrupt a computation by invoking the respective command
from the Eval menu. The keyboard shortcut is Ctrl-I.
Unlike with other ML systems, you can also put a computation into background instead of
interrupting it, by pressing Ctrl-Break:
loop ()
val it : '1 = _future
This feature is useful if you still want to use the result of the computation later on but do not
want to wait for it now. The result of a background computation is represented by futures.
The interactive toplevel (in text mode) is exited with Ctrl-Z, or by calling exit().
The Inspector
For more complex values, plain static output of the result is often insufficient. The Alice system
includes the Inspector for browsing arbitrary data structures interactively:
inspect (List.tabulate (10, fn i => (i, i*i, i*i*i)))
val it : unit = ()
An Inspector window will pop up displaying the table of square and cubic numbers:
When you inspect additional values, they will be displayed in the same Inspector window. The
inspector can be used to browse arbitrary data structures:
val r = ref 56
val r : int ref = ref 56
inspect (3 + 8, SOME 3.141592, {hello = "hello,
world"}, r, [true, 4 < 3])
val it : unit = ()
Note: If the size of the inspected data structure exceeds certain configurable limits, parts of the output will be hidden.
Those parts are represented by dots, which can be interactively unfolded if desired.
Note the index put after the ref constructor in the Inspector window. All displayed references
and futures are decorated by such an index to indicate which of them are identical.
The Inspector concurrently watches the data structures it shows. If they change, it will
automatically update its display. For example, if you re-assign a reference the Inspector window
will be updated automatically:
r := 33033
val it : unit = ()
You can select any part of a displayed value by clicking on it. If you hover over the selection
with the mouse the inspector will show the value's type as a tool tip.
You can clear the Inspector window by choosing Clear from the Inspector menu.
________
Laziness ____________________________________________________
While Standard ML is a fully eager (strict) language, Alice ML provides support for optional
lazy evaluation. Any expression can be evaluated lazily by preceeding it with the lazy keyword:
val x = lazy 4+5
val x : int = _lazy
Lazy suspensions are represented by the _lazy notation in the interactive toplevel's output, as
shown above. The value of x will not be computed before it is actually required. For example,
(x,x)
val it : int * int = (_lazy, _lazy)
x
val it : int = _lazy
fun pair x = (x,x)
val pair : 'a -> 'a * 'a = _fn
pair x
val it : int * int = (_lazy, _lazy)
x > 10
val it : bool = false
x
val it : int = 9
Tupling is parametric in its components and does not trigger x. Neither does applying it to a
function whose body does not trigger its argument, like pair above. The comparison operator >
is future-strict in its arguments, however, and hence forces evaluation of x. Likewise, pattern
matching, arithmetic operations, comparison (op=) or similar operations can force a value.
________
Concurrency _________________________________________________
Alice extends SML with support for concurrency. Concurrency is light-weight: the system can
handle tens or hundreds of thousands of concurrent threads. Concurrent programming in Alice is
uniformly based on the model of futures.
A concurrent thread can be initiated by means of the spawn expression:
spawn 45*68
val it : int = _future
In this example the value 45*68 is computed in a new thread. The result of the spawn expression
is a future, a place-holder for the result of the concurrent computation. Once the result becomes
available, the future will be globally replaced by the result. We say that threads are functional, in
the sense that they have a result.
Note: If you do not happen to have an ancient machine then you most likely will not have seen the output above. Instead
you have already seen the actual result of the computation, because it performed faster than the interactive toplevel was
able to print it.
The semantics of futures becomes more obvious if we look at a thread that does not terminate
immediately. For that purpose, let us define the naive version of the Fibonacci function, which
has exponential complexity:
fun fib (0 | 1) = 1
| fib n
= fib (n-1) + fib (n-2)
val fib : int -> int = _fn
On an ordinary desktop PC, computing fib 35 will take quite some time. We perform that
computation concurrently:
val n = spawn fib 35
val n : int = _future
We get back a future, that we can look at using the Inspector:
inspect n
val it : unit = ()
At first, the Inspector will display the value as a future, as shown above. You can investigate the
type of the value the future is holding place for by using the tool tip feature:
Once the computation of the result finishes and the thread terminates, the future gets replaced by
the thread's result. The Inspector will update its display accordingly:
The situation becomes more interesting if we start several threads at once:
inspect (List.tabulate (10, fn i => spawn fib (i+25)))
val it : unit = ()
The individual entries from the small table we build are calculated concurrently and become
available individually. At some point in time the Inspector window might display the following:
Data-flow synchronization
Futures can be passed around as values. Once an operation actually requests the value the future
stands for, the corresponding thread will block until the future has been determined. This is
known as data-flow synchronisation and is a powerful mechanism for high-level concurrent
programming. Synchronisation on futures can also be done explicitly, by employing the library
function await:
spawn fib 30
await (spawn fib 30)
val it : int = 1346269
Interestingly, lazy suspensions are nothing but a special form of future, called a lazy future.
Requesting a lazy future is triggering the corresponding computation. Consequently, await can
also be used to trigger lazy computations explicitly:
lazy 3+4
val it : int = _lazy
await it
val it : int = 7
Atomicity
Dealing with state correctly in a concurrent environment requires the availability of atomic
operations. Alice provides an atomic exchange operation for references:
val r = ref 10
val r : int ref = ref 10
Ref.exchange (r, 20)
val it : int = 10
r
The value of the reference is extracted and replaced in one atomic operation.
Download