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.