a gentle (but very rapid) introduction to Spice, v1.3, 23rd September 1998

1 introduction

This document is a thought-sorting draft of an introduction to Spice. It's intended to be read by:

table of contents


1 introduction
2 simple expressions
  2.1 hello, world
  2.2 1, 2, buckle my shoe
3 variables
  3.1 typed variables
4 function definitions
  4.1 typed arguments
  4.2 typed results
  4.3 indefinite arguments
5 arrays
6 dot and infix notation
  6.1 dot and infix function definition
7 ifs and buts
  7.1 elselessness and elseif
  7.2 short conditionals
  7.3 unless
8 simple loops
  8.1 while and break (and return)
  8.2 until loops
  8.3 for
9 multiple values
  9.1 void context
  9.2 multiple-value loops
  9.3 explode
10 procedures as values
  10.1 first-class procedures
  10.2 lambda-expressions
  10.3 hole-expressions
  10.4 combining functions
11 classes
  11.1 class definition
  11.2 updaters
  11.3 methods
  11.4 constructors
  11.5 inheritance
  11.6 overloading functions
12 type-expressions and "as"
  12.1 union types
  12.2 multiple
  12.3 lots
  12.4 arrays (and based arrays)
  12.5 optional types
  12.6 "as" (and based arrays)
13 modules
  13.1 public and private
  13.2 imports
  13.3 readonly
  13.4 qualified access and aliases
  13.5 pervasive import
14 switches
15 styles and properties
16 numeric literals
17 units
18 enumerations
19 symbols and tables
20 all sorts of unmentionable things
  20.1 extended relational expressions
  20.2 op
  20.3 super and extends
  20.4 shared slots
  20.5 properties
  20.6 exceptions
  20.7 memo-expressions
  20.8 parallel iteration
21 the standard library

2 simple expressions

In this and subsequent sections we assume the reader has access to a Spice system into which they can type Spice code and have it executed.

2.1 hello, world

A long-standing Unix-spawned tradition is that one's first program in a new language should be the one that prints "Hello, World" or some suitable variant, so here it is in Spice:

println( "hello, world." );

Pasting this into your Spice evaluation should result in "hello, world" being displayed somewhere obvious.

"hello, world." is a string literal representing a sequence of characters. Most characters can appear as themselves in a string, but specific exceptions are all three quoting characters (string quotes ", reserved quotes ', and symbol quotes `) and the escape character backslash.

println is the name of a built-in procedure. We'll see later that Spice has several kinds of procedure, including functions, methods, and constructors; we use the term "procedure" to refer to them all without distinction.

The syntax F(X) is one form of procedure call; it evaluates the procedure F (which in this case is easy) and the argument(s) X (ditto) and then calls the procedure, supplying it with the values of the arguments. The procedure does something (such as printing its argument) and returns some results, which we throw away.

The semicolon is a statement separator, signifying the end of one statement and (possibly) the beginning of another. Spice scripts often don't need them, because Spice treats newline as a semicolon in many ways, but it can only do this when it knows how the next line starts -- and we don't want to type the next line yet.

2.2 1, 2, buckle my shoe

println isn't restricted to strings; it can do numbers, too.

println( 1 );

println( 1_032 );

println( 40 + 2 );

Evaluating these should result in the values 1, 1032, and 42 being printed. The underbar in 1_032 is just a visual separator. Spice has the "usual" arithmetic operators, +, -, *, and /, although they have some extra wrinkles we'll meet later.

Aside. It is wise to write Spice operators with surrounding spaces. For the reasons why, see the later section on numeric literals.

What's more, println can take multiple arguments:

println( 1, 2, "buckle my shoe" );

which will print 1 2 buckle my shoe. Note that the second and subsequent arguments are printed preceeded by a space, and that there's only one newline printed, at the end. The degenenerate case println() with no arguments just prints a newline.

3 variables

You can declare variables to hold values.

var x = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10;

println( x );

which will print 55. The value x holds can be changed by assignment:

x = x * 10; println( x );

will print 550. Using the word const instead of var makes an unassignable variable (rather an oxymoron, but there it is).

Variables must be declared before they are used, either for their value or to assign to them. If you try to use an undeclared identifier, the Spice compiler will complain. (Usually it will take a recovery action which is to declare the identifier for you as a top-level variable, but your code is still technically illegal. It's so easy to declare variables in Spice that we encourage writers to disable this particular recovery action.)

3.1 typed variables

It's possible to specify that a variable is restricted to holding values of a given type. For example,

var count = 0 is Int;

# or var count: Int = 0;

count = "of Monte Christo";

declares count as a variable of type Int, restricted to integer values and then throws an error when it attempts to assign a string (of type String) to it. If a variable is declared with a type but no initial value, then the default value for that type is used to initialise the variable; the default value of type Int is 0:

var anInt is Int; println( anInt );

will print 0. If there's no type then the variable is declared to be of type Any (which means what it sounds like); the default value of Any is called absent (and sometimes null, but absent is preferred).

4 function definitions

There's no magic about procedures that the user cannot harness. A simple way to define your own procedure is to write a function definition:

define function add( x, y ) => x + y enddefine

define function subCStyle( x, y ) { x - y }

println( add( 1, 2 ), subCStyle( 4, 3 ) );

Two functions are defined, add and subCStyle, and then invoked in the usual way, resulting in 3 1 being printed. Spice allows two styles of definition; in the first, the function body starts after the => symbol and extends until enddefine; in the second, it is wrapped inside C-style braces. This is a general rule of Spice: C-style constructs are supported, but the closing-keyword style is also available.

Note that there's no need to use return to specify the procedure's return value; the last expression executed in a procedure will specify its result in all the simple cases.

4.1 typed arguments

The arguments to a function can be typed in the same way that variables can.

define function addChecked( x is Int, y is Int ) => x + y enddefine

Calls to addChecked with non-Int arguments will throw an error. Spice can sometime take advantage of the type information to produce better code or more informative error messages. Later on, we'll see how type information can be used to write polymorphic or overloaded functions.

4.2 typed results

The result of a function can also be typed.

define function addToInt( x, y ) returns Int => x + y enddefine

The result from addToInt must be an Int value; if not an error will be thrown. Again, the Spice compiler may be able to use this information to generate better code or error messages.

4.3 indefinite arguments

How does println manage to have arbitrarily many arguments? It uses an indefinite argument, marked with the ellipsis notation:

define function gather( rosebuds... ) => rosebuds enddefine

println( gather( "while", "ye", "may" ) );

rosebuds is an indefinite argument whose value is all of the (remaining) arguments to the function. This prints as [while ye may], which is the way array values are printed; the arguments are turned into an array. (We'll see more about arrays soon.)

It's possible to have one or more definite arguments before the final indefinite argument:

define function foo( x, y... ) => println( x ); println( y ) enddefine

foo needs at least one argument x but takes arbitrarily many more, which are gathered into y. It then prints both x and y; if there are no extra arguments, then y is an empty array, which prints as [].

foo has two statements in its body, separated by semicolons. The result from foo is the result (whatever it may be) from the last call of println; println returns its last argument as its result, so foo returns the array of its remaining arguments arguments as its result.

The name of the indefinite argument can be omitted, in which case the name arguments is assumed.

5 arrays

Spice allows arrays to be constructed using array expressions, which are expression sequences enclosed in square brackets:

var lots = [27, "now", "is", "the", "time", 42]; println( lots );

which will print [27 now is the time 42]. Arrays can contain values of any type (unless otherwise specified). Individual elements can be extracted (and updated) by indexing:

println( lots[1] ); lots[1] = "anguish"; println( lots );

which will print now and [27 anguish is the time 42. Spice arrays are by default based at 0, so element 1 is the second element, but an array can be based at any integer value using the function rebase:

var newlots = rebase( lots, 1 );

newlots[1] = "first", println( newlots ), println( lots );

which prints [first anguish is the time 42], twice. rebase does not alter its array argument; it makes a new array which shares the same collection of elements, so updating one updates the other.

The function length delivers the number of elements in its array argument, and the function arrayBase delivers the base of its array argument:

println( length( lots ), arrayBase( lots ), arrayBase( newlots ) );

which will print 6 0 1.

6 dot and infix notation

It's often useful and clearer to write calls to one-argument functions, expecially those that just extract components from structured values like arrays, using dot notation:

println( lots.length, lots.arrayBase, newlots.arrayBase );

is just another way of writing the previous calls. In fact you can call procedures of any number of arguments using dot notation, because of a general rule of the language:

X.f(Y) == f(X,Y) == X @f Y, and X.f == f(X) == X @f

aside. We'll see later that when p has bee declared to be a property, the expression x.p is not equivalent to p(x); this is to retain backwards compatability with ECMAScript.

The forms X @f Y and X @f are a way of writing any function call as an infix or postfix expression, which is often useful when you have a long chain of function applications each of shich supplies the "left" argument to the next.

For example [1,2,3,4].rebase(1), or equivalently [1,2,3,4] @rebase 1, are infix ways of calling rebase. The principal difference between . and @ is that . is much more tightly binding -- it's the most binding infix operator -- and @ is loosely binding, being only just tighter than the logical connectives && and ||.

Different styles of function call are used to make it clear what's an argument to hat.

6.1 dot and infix function definition

Although the plain fucntion definition syntax we've seen already is enough, because of the equivalence of the different notations we've described, Spice allows you to define a function in the style in which you expect it to be called:

define function x @addWithAt y => x + y enddefine

define function x.addWithDot(y) => x + y enddefine

The arguments can still be typed using is, but it can look rather clunky, so you can use the alternative syntax for is, which is :, and for a function defined with dot-notation, the special on syntax:

define function x:Int @addWithAtColon y:Int => x + y enddefine

define function on Int x.addWithDotColon(y:Int) => x + y enddefine

All four of these definitions define functions which add two values together, the second pair demanding that those values be Ints, and all of them can be called in all the different ways we have discussed.

We'll see later how these notations tie into the more common object-oriented approaches, and why the on syntax is so-called.

7 ifs and buts

So far, all of our expressions have been unconditional -- get the arguments, do something, deliver the result; no choices are involved.

Spice has conditional expressions for expression choices, and relational expressions for tests.

if lots.length < 10 then "short" else "long" endif.println;

The relational expression lots.length < 10 compares the length of lots (which is probably still 6) with 10 and delivers true if it's less than and false if it's equal or greater. true and false are the built-in values of type Bool. Spice also has the obvious >, <=, and >= relations, and the not-so-obvious == for equality and != for inequality; != can also be spelled /==.

Spice also supports the traditional C-style if construct:

if (Expr) Statement else Statement

The conditional expression tests the boolean value; if it is true its value is its then arm, if it is false its value is its else arm, and if it's not boolean, an error is thrown. (Unlike various other languages, 0, the null string "", and the null reference null (or absent), are not alternatives to false, and values such as 1, "yes", and println are not alternatives to true.)

println is being called here with dot notation, with its argument being the result of the if-expression, here the string short.

7.1 elselessness and elseif

The else part of a conditional is optional. If you miss it out, it's as though you'd written one with no expression following. Where you don't want a result from the if-expression, there's no problem; if you do, it will usualy generate the value absent. The exact rules will be described later.

If you want to write a chain of tests, rather than ending up with ever-more-deply nested if-endif structures, Spice lets you introduce additional tests with elseif:

if x == "hello" then

println( "well hello there" )

elseif x == "goodbye" then

println( "sorry to see you go" )

else

println( "eh? what does", x, "mean?" )

endif

You can have as many elseifs as you need.

7.2 short conditionals

Spice also permits "short" conditional expressions, for which it uses the same syntax as C (because most of the alternatives have all been taken): the expression X ? T : F is equivalent to if X then T else F endif.

7.3 unless

Spice has an alternate form of if, the unless (with closing keyword endunless) which is the same except the sense of the (first) test is inverted.

8 simple loops

Spice has several loop structures, falling into two main categories; while loops, which iterate until a condition is satisfied, and for loops, which iterate over collections.

8.1 while and break (and return)

Suppose we want to iterate over the elements of an array lots. Here's one way to do it with while:

var i = 0;

while i < lots.length do ... lots[i] ...; i += 1 endwhile

The expression i += 1 is equivalent to i = i + 1, but more compact, especially when i is some complex expression.

For example, to find the element of an array containing a specific element by linear search:

define function findIndex( a, x ) =>

var i = 0;

while i < a.length do if a[i] == x then return i endif; i += 1 endwhile;

absent;

enddefine

The return expression evaluates its operand (if any) and then exits from the procedure with those results as the procedure result. == compares the element a[i] with x using whatever equality test is defined by the type of x. If the while loop terminates, then the final expression in the procedure body, absent, is the procedure result.

There's another way to write this. In Spice, while loops are expressions, and can deliver values.

define function findIndex( a, x ) =>

var i = 0;

while i < a.length with result absent do if a[i] == x then break i endif; i += 1 endwhile;

enddefine

The result from a while is given by any break it executes; a break terminates the loop and, if it has an operand, delivers that as its value. If the break is never executed, then the loop's result is given by its with result clause.

8.2 until loops

Spice has an inverted while loop, the until loop, which is introduced with until, ends with enduntil, and is the same except the sense of the loop termination test is inverted.

8.3 for

Spice has several styles of for loop. The simplest is the from-to variant, in which we can write our linear search:

define function findIndex( a, x ) =>

for i from 0 to a.length - 1 with result absent do

if a[i] == x then break i endif;

endfor;

enddefine

The variable i is automatically declared as a varible that exists only while the loop is executing; it takes values from the from value (here, 0) to the to value (here, a.length - 1) inclusive. Like the while loop, for loops are expressions, and can deliver values.

The other principal variant, which isn't quite so useful here, is the in variant:

define function findIndex( a, x ) =>

var x = 0

for ax in a: Array with result absent do

if ax == x then break i else i += 1 endif;

endfor;

enddefine

ax is automatically declared, and takes its values from successive elements of the array. However, since we want the index i of the element, we have to maintain a count ourselves. Later on we'll see other iterations that are more effective.

9 multiple values

So far, we've seen Spice expressions and procedures which deliver single results. Spice expressions can deliver multiple results, often avoiding the need to construct new objects or to use "reference parameters" (which Spice hasn't got).

define function plusOrMinus( x, y ) => (x + y, x - y) enddefine

The comma keeps both its left and right operand values (exactly as it does in a function argument list); plusOrMinus delivers two values.

println( plusOrMinus( 20, 17 ) );

will print 37 3.

9.1 void context

Some expressions, in particular the left operands of semi-colon and top-level expressions, are evaluated in void context; they are evaluated and their results thrown away. The Spice system you are using to test our examples runs them in void context.

Sometimes you need to make it explicit that, whatever else would have happened, you want no values to escape. The expression none E evaluates E in a new multiple-value context and then throws away all the answers, delivering none. The expression none is a shorthand for none (), representing no values. Similarly, the expression one E evaluates E and turns it into one value: if E delivered no values, one returns absent; if E delivers one value, one returns that; and if E delivers several values, one returns the first one.

9.2 multiple-value loops

Loops discard the results from their bodies by default. To get the results from all the iterations, you have to follow do with the keyword all:

println( for i from 1 to 10 do all i*i endfor )

to print 1 2 9 16 25 36 49 64 81 100.

9.3 explode

One built-in function produces multiple results as a matter of course; explode. explode takes one argument and explodes it into its constituents (if any) as a multiple result. Applied to an array, it explodes it into its elements; applied to a string, it explodes it into its characters. Applied to an atomic object (a number, a character, a boolean, absent) it delivers no values.

10 procedures as values

Spice procedures are not restricted to being defined and called; there are several ways they can be manipulated.

10.1 first-class procedures

Spice procedures are first-class values; they can be passed as parameters, return as results, and stored into variables and data-structures (such as arrays; we'll see more later).

This is particularly useful when you need to do something to all the elements of a collection, but the details of how the collection are kept is secret (eg to allow you to change it later); you write a function that walks over the collection and does "something" to each element. Since all we've got is arrays, that forms our first example; let's suppose that them is a secret array variable.

define function appCollection( f ) =>

for x in them: Array do x.f endfor

enddefine

Now the expression appCollection(println) or, equivalently, println.appCollection, will print all the elements of them. If the elements do something sensible when passed to some function mangle, then mangle.appCollection will mangle all of them.

appCollection isn't as general as it might be; f might deliver multiple results, but each call of f gets discarded (because it's in a loop body). But we can fix that:

define function appCollection( f ) =>

for x in them: Array do all x.f endfor

enddefine

Now appCollection returns all the values that f returns for the caller to do with as they like. Of course, we might want to construct a new collection from the old one by mangling each element:

define function mapCollection( f ) => [f.appCollection] enddefine

which gathers up the results from appCollection and makes a new collection (here, array) out of them.

10.2 lambda-expressions

Passing functions as arguments, and applying them, is all very well, but it can get tedious defining trivial functions to use. For example, if we want to make a new collection by incrementing every element of the old one, we can write:

define function inc( x ) => x + 1 enddefine

inc.mapCollection.println;

If them is, say, the collection [0 8 41] then this will print [1 9 42]. But it's tedious to have to define a function, put it somewhere in the code, give it a suitably mnenomic (but short) name, then then pass it. Wouldn't it be nice if you could write the function right where you were going to pass it?

Of course the answer is, "yes", otherwise we wouldn't have posed the question. The expression (A => E), where A is an argument list (with the brackets omitted) and E is a sequence of expressions, is called a lambda expression (for historical reasons to do with Church's lambda calculus) or a procedure literal. Lambda-expressions are intended to be used for short procedures; otherwise they can make the code look cluttered.

Instead of defining inc above, we can write (x => x + 1).mapCollection.println for the same effect. To double each element, use (x => x * 2); to square it, use (x => x * x); to replace it with 0 use (x => 0).

10.2.1 "full lexical scope"

Because we haven't said you can't, the reader may suppose that you can write lambda-expressions within procedures, and similarly, that those expressions may refer to, even assign to, local variables of those procedures. And you can.

define function konst( x ) => (ignored => x) enddefine

konst (so-called because const is a Spice reserved word) is a function that makes constant functions; konst(E) is a function that delievrs the value of E whatever it's applied to, so konst(0) is a function [of one argument] that always delivers 0. The variable x which is the parameter to konst is available to the lambda-expression even after konst has finished executing.

This behaviour -- that the variables of a function are visible to lambda-expressions within that function, and that they live on after the function exits -- is called full lexical scoping. It's relatively uncommon in programming languages, but it is your friend.

10.3 hole-expressions

Lots of little lambda-expressions are just an operator (or function call) with one (or two) arguments "missing"; like inc and its brethren above. For these Spice allows you to write a special form of lambda-expression, the hole expression, which is a function call or operator invocation with some of its arguments replaced by holes. The usual hole is written _, and it stands for "the [first] argument"; it makes the application or invocation into a lambda expression.

So (_ + 1) (the brackets are unnecessary) is another way of writing (x => x + 1), where x has been replaced by the hole and the argument declaration is unnecessary.

(_ - 1) is the function that subtracts 1 from things, (42 - _) is the function that subtracts things from 42, and (_ * _) is the function that squares things.

Just in case you ever want to use holes for functions with more than one argument, there are as many holes as you like, written _1, _2, and so on; _ is shorthand for _1. A hole-expression has as many implied arguments as the biggest hole it uses; the Spice compiler will be curious about expressions with missing holes (eg _19 + 1).

10.4 combining functions

Because it's possible to write functions that take and deliver other functions, its possible to write combining functions who's job is just to manipulate other functions; this can result in a style known as higher order programming.

The standard function Then [note the capital T!] takes two functions f and g and returns a new function that takes some arguments X, applies f to them, and then applies g to the result; it might be written as:

define function Then( f is Function, g is Function ) is Function =>

(args... => args.explode.f.g)

enddefine

Note that explode generates arbitrarily many results, all of which become arguments to f; all of fs results become arguments to g; and all of gs results become the results of the lambda-expression. Nothing special has to be done by the programmer to keep tabs of how many arguments and results are returned.

11 classes

So far we've only uses (some) of the built-in Spice types: integers, strings, arrays, and procedures. Spice also allows you to define your own data-types using classes.

11.1 class definition

A class definition allows you to define a new (kind of) object which you can use in your code. The simplest class definition introduces an object with named slots:

define class Pair

slot front = absent is Any

slot back = absent is Any

enddefine

This defines a new class object, Pair, with two slots called front and back. The slot declaration is just like a var declaration, except that it makes variables "inside" objects; you can leave out the initialiser and type in the usual way.

The name Pair becomes a new type name, and you can use it in the type part of a variable declaration or procedure argument.

You can print out the class if you wish; Pair.println will print something like <Class <Pair front:absent back:absent>>. (We say "something like" because it's possible to change the default way objects get printed.)

You can see the values in the slots. front and back are actually procedures and you call them in the usual way; Pair.front and Pair.back. Of course, this is pretty useless, because you know what's there, and you can't change the values of slots in classes.

You can make new Pair objects with the expression new Pair(). Each such object also has a front and a back, which start off as copies of the values in the class, and which you can change. Every object also responds to the function typeof by delivering its class object.

11.2 updaters

Suppose we've declared var p = new Pair(); so that p holds a new Pair object. We can assign to the front and back slots of p:

p.front = 1, p.back = "two", p.front.println, p.back.println;

prints 1 two. In Spice, it's possible to call procedures on the left-hand-side of an assignment, in which case what happens is that the updater of that procedure is called. Just as Spice makes procedures to access the slot-slots of a class value, it gives them updaters to change those values.

technical note. Unlike some languages (Common Lisp, Dylan), it is the procedure value that has the updater, not the procedure name. This means that you can call the updaters of procedures passed as parameters or stored into data structures. We stole/////re-used this behaviour from Pop11.

You can define your own updaters if you wish:

define function x.foo = y => println( "updating", x, "with", y ) enddefine

The = y says this this is defining the updater of foo (and there had better be a definition for foo elsewhere; you can't define the updater of a procedure that doesn't exist) and that y is the name of the parameter which is the "right-hand-side of the assignment". y can be typed, as usual.

While an updater can do whatever it likes, it is very stongly advised that updaters are given only to access functions, and that they do the "obvious thing"; after an assignment x.foo = E then the expression x.foo should deliver the value E that was assigned.

If foo is a procedure, then foo.updater is the updater of that procedure, if it has one, and absent otherwise.

11.3 methods

A class can also define methods which act on objects of the type it defines.

define class Pair

slot front = absent is Any

slot back = absent is Any

define method wipe() => this.front = absent; this.back = absent; none enddefine

define method setPair(x,y) => this.front = x; this.back = y; none enddefine

enddefine

This version of Pair defines two methods, wipe and setPair. wipe sets both slots to absent, and setPair sets them to the given parameter values. The name this is bound to an implicit additional parameter, which is the instance of Pair to be changed.

The expression none delivers no values; there doesn't seem to be a sensible value to deliver from either of these methods.

Methods are procedures, and you call them in the usual ways; if p is an instance of this Pair, then p.wipe will wipe it, and p.setPair(1,2) will set its front to 1 and its back to 2. You can tell from this that this is bound to the first argument value. In fact, if it takes your fancy, you can use a different name than this, using the same function-definition syntax that we saw earlier:

define method self.wipe => self.front = absent; self.back = absent; none enddefine

would be a suitable replacement for Pair's version of wipe.

We'll see more about methods when we discuss inheritance, later.

11.4 constructors

The new Pair() syntax is a bit limited, because you can't easily set new values for the slot-slots. If you want to create a new object and set the values of its slots, you must define a constructor. Here, Pair gets a third outing:

define class Pair

slot front = absent is Any

slot back = absent is Any

define method new pair(x,y) => this.front = x, this.back = y enddefine

enddefine

You can make a new initialised Pair by writing new pair("hello","world"). This works by making a copy of Pair and then handing that on as the this for pear. Any values returned by a constructor are discarded; the result of a new expression is the newly constructed object.

You can have as many constructor definitions as you like for a class. As a special favour, you can use method new P inside a definition of a class P to define overloadings of its constructor call.

Aside. We've now met all the types of procedure you can have in Spice; functions, methods, constructors, and the different flavours of lambda-expressions. There are predicates which can tell them apart, although you will usually only need these when you're debugging code.

11.5 inheritance

A class can extend an existing class. Here's one example; we might choose to make triples an extension of pairs.

define class Triple extends Pair

slot side

define method new triple(x, y, z) => this.pair(x,y), this.side = z enddefine endclass

Triple has all the slots that Pair does, and one more: side. front and back automatically work on Triples. Triples are Pairs, and then some. The constructor triple works by first invoking the constructor pair to do the first two elements and then assigning the third itself.

Not just front and back, but any function that works on Pairs and doesn't explicity include non-Pairs will work on Triples.

11.6 overloading functions

How does this happy state of affairs come about? In Spice, functions can be overloaded; they can have different definitions which apply to different argument types. Here's a trivial example. We've chosen to use the short syntax version of is, which is :, in the definitions:

define function size( x: Int ) => x @logToBase 2 enddefine

define function size( x: String ) => x.length enddefine

define function size( x: Any ) => 0 enddefine

This defines the size of an integer to be its bitswidth (and blows up if x is 0), the size of a string to be its length, and the size of anything else to be 0. The choice of which definition of size to use is made when size is called, by looking at the types of its arguments.

When the argument type is the name of a class (like Pair), then it will accept values of any of its extensions (like Triple), unless there is a definition for that extension, in which case that definition "wins". This is how method definitions work; they are overloaded on their this (ie first) argument.

It's possible to overload functions on more than one argument, but early versions of the Spice compiler will apologetically reject such definitions; your compiler may or may not be such an early version.

12 type-expressions and "as"

So far we've said little about the type-expressions that can be written following is, except that certain built-in names (Any, Int, String) are allowed, and the names of any classes.

In fact types can be expressed with expressions, which have the same syntax as value-expressions, but with different meanings for some of the built-in operators. There are five important type-expressions in Spice; the union, multiple, lots, array, and non-optional types.

12.1 union types

If T and U are types, then T|U is the union of T and U, the type of values which are either Ts or Us.

12.2 multiple

If T and U are types, then T, U is the multiple type which is a value of type T then a value of type U.

12.3 lots

If T is a type, T** is the lots of T type, which is some unspecified number of Ts as multiple values. The return type of a procedure defaults to being Any**.

12.4 arrays (and based arrays)

If T is a type, then T[] is the type array of T or T row. Note carefully that the Spice array constructor [E] always makes values of type Any[], because Spice arrays can hold any kind of value; to make eg an Int[] you have to invoke a special constructor IntArray(E).

The type T[N], where N is an integer expression which the compiler can evaluate, is an array of T based at N. If N is omitted, it defaults to 0, so T[] expects 0-based arrays. See the entry for as below for some interesting alternatives.

12.5 optional types

Unless otherwise specified, absent is not permitted where a value of a specific (non-Any) type is expected; so a variable typed as Int cannot legally be given absent as its value. This can be over-ridden by using the optional type constructor; if T is a type, T?? is the type which all the values of type T and also the value absent.

12.6 "as" (and based arrays)

So far we've seen is (and its shorthand, :) used for typing variables and arguments. There's another type-word, as, which you can use. A variable (or argument) typed with as has the type given to it but, whenever you assign to it or initialise it, the value you assign is converted to that given type by running the type's conversion procedure. For a type named T the conversion procedure is traditionally called toT.

The most important use for this is that the conversion procedure for the based array type T[N] will use rebase to convert the assigned value (if it is an array) to a new array based at N; Spice promises that if the assigned value already is an N-based array, it will be passed through unchanged.

13 modules

So far we've seen fragments of Spice code out of context for illustrative purposes. In real life, Spice code code should be organised into modules. A module is a collection of procedures, variables, and classes (and styles, which we'll see later) which work together to provide a coherent service to the programmer who uses those modules.

A module starts with a module header, which identifies the module and the version of Spice it was intended for, specifies any other modules it may need, and then continues with the module body, which is a sequence of variable, procedure, class, and other declarations, or the kind we've discussed elsewhere.

The header starts with the spice specification part, which consists of the reserved word spice, a version string, and possibly some preference settings. The version string is a string specifying the version of Spice that the module is supposed to work with, and the preference settings control the Spice compiler -- see later for details.

Otherwise the header starts with the reserved word module and the module name, which is a series of simple names deparated by dots. For example,

spice "release.2.0";

module this.is.an.example;

Spice does not constrain module names to conform to file names on your local system, but it's a good idea to put a module whose name ends in .foo in a file called foo.

technical note. An IDE for Spice is required not to insist that module names form a path through a fileing system hierarchy, and is required to accept module name components longer that the local filing system component names, and with "strange" characters in.

13.1 public and private

A module exists to make a service available to its users, which it does by making some of its identifiers visible to those users. By default, none of the identifiers in a module are visible. However, declarations can be marked as public by writing declaration qualifiers in square brackets after the introductory keyword. To make a public variable, for example:

var [public] exposed = "hello" : String

declares a public string-only variable called exposed. You can mark a variable private instead, but since this is the default, it won't make much difference.

You can declare a function public in the same way:

define function [public] inc( x ) => x + 1 enddefine

More importantly, you can declare a class public. If you do so, the class name is public, and so are all the non-slot methods declared in it. The slot methods (ie the names of slot-variables) are still private by default; you can declare them public explicitly, and you can declare the other methods private explicitly.

Incidentally, private and public are not reserved words.

13.2 imports

public is one side of the coin; imports is the other. A module can import another one in its header.

module using.example;

import this.is.an.example;

This makes all the public identifiers from this.is.an.example visible inside using.example. If you don't specify where this other module is to be found, the Spice compiler will use a set of rules to locate it; but you can force its hand:

import this.is.an.example from "/users/hedgehog/example.spi";

import this.is.an.example from "http://cdollin/Spice/Modules/example.spi";

Following the from is an expression (almost always a string literal) with the URL for the source of the module.

Imported modules need not be written in Spice; Spice can import CSS stylesheets and (eventually) a subset of Javascript. Maybe even Java ...

13.3 readonly

It's possible to qualify public variables and slots as read only, which means that they can be read but not changed by importing modules. For ordinary variables, all that need be done is to prohibit assignment to them; but for slots, which are accessed by procedures, it's not quite so simple.

When a slot is marked read-only

slot [readonly, public] magic = 42;

then the compiler makes two versions of magic; one for local use, and one for export. Only the local version has an updater. This can cause the interesting situation:

module defines.magic;

define class P

slot [public, readonly] magic = 72;

enddefine

define function isEqual( x ) => println( x === magic ) enddefine

endmodule

module Q

import defines.magic;

testEqual( magic );

endmodule

which will print false as the exported magic is a different function to the local one.

13.4 qualified access and aliases

It's possible to import a module, but to declare that you need to specify its name everywhere you specify its variables:

import [qualified] defines.magic;

To refer to magic now you need to prefix it with the last part if its module name: magic::magic. (Requiring the entire module name would be overkill; module names are supposed to be long and descriptive.)

The main use of qualified is to allow an imported module to define lots of useful names (usually constants) without polluting your own modules namespaces.

If you don't like the last part of the module name, or if it would be ambiguous, you can change the part you specify:

import [qualified(boo)] defines.magic;

Now boo is an alias for defines.magic and we can write boo::magic.

13.5 pervasive import

A module that imports some identifiers -- say, brick and concrete -- from another module does not by default re-export them; they're not part of its public interface. They can be injected into the public interface by qualifying the import with public:

import [public] defines.magic;

Now this module exports magic::magic.

14 switches

The power of Spice's overloaded functions means that you won't write switches so often as you might in a more conventional language. But you still need to be able to choose from finite selections of integer and string values, so Spice has a general switch construct:

switch x into

case 1: statement1();

case 2, 3: statement2();

case "hello": case "world": statement3();

default: statement4();

endswitch

If x has the value 1, then statement1() is executed. If it has the value 2 or 3, then statement2() is executed. If it has the value "hello" or "world", statement3() is executed. And if it has none of these values, statement4() is executed.

Note that the values in the switch can be integers or strings (or symbols); indeed the case values need not be compile-time constants. (However, the switch construct is more efficient with compile-time constants, and more efficient with integers than with strings and symbols.) If a switch has a compile-time case label of N then that takes priority over any run-time case label evaluating to N.

Also note that there's no need for a break or similar construct to leave the switch; the case body starts with its first statement and ends just before the next case or default. (You can, of course, only have one default per switch).

If there's no default, it's as though default: none; had been written.

15 styles and properties

[Extensive but to-be-done.]

16 numeric literals

So far we've taken numbers pretty much for granted, writing them as digit sequences with the occasional _ for visual clarity. Spice has a rather extensive set of numeric literals and they don't all represent numbers.

A numeric literal starts with a digit and consists of a series of digits, letters, underbars, dots, colons, plus signs, and minus signs. This allows all the usual numbers: 42, 1_024, 1_000_000, 0xff, 98.4, 273.16. But it also allows odd literals like 47px, 100red, 192.2.4, 10:13pm, 4-July-1998. Whatever can these mean?

There are a few simple rules. First, a numeric literal consisting entirely of digits and underbars is a decimal integer and means what it looks like it means. Second, a numeric literal that consists of one or more digits, a dot, and then one or more digits is a floating literal, and that means what it looks like it means.

A numeric literal that consists of a series of decimal digits, the letter x (in either case), and then a series of letters and digits is a based integer; the number before the x is the base, and the alphanumerics after it are the digits in the extended base, with a (either case) being digit 10 up to z (either case) being digit 26. If there are digits that don't fit the base (8x.9, 16x.gh) it is an error. The effect of 16x. can also be obtained (for compatability reasons) by 0x.

A numeric literal that consists of a decimal integer or a floating literal, followed by an optional dot and then a series of letters, is a unit literal, representing a measurement with the magnitude given by the number and units given by the letters (which form the unit name); for more details, see later.

technical note. This means that 0xfe is ambiguous; are this based integers or unit literals? It is a based integer. To get a based integer to have a units specification, you must put in the "optional" dot, so that you write 0xf.fe to get 15 in units fe.

17 units

To support its use in laying out Web pages (and for other graphic operations), Spice values can have units. Several units are built-in to the language, notably inches (in), millimeters (mm), points (pt), and pixels (px).

Writing a unit literal is easy, as described earlier; 4in is four inches, 27mm is twenty-seven millimetres, 100px is one hundred pixels. These pre-defined names are also available as postfix operators, so x in and (y + 1)px are legal, and by special dispensation of the dot operator can appear as functions using dot, so x.pt is also legal.

Thus 100.px is legal, as a unit literal, and 100 .px (with a space) is legal, as the integer 100 passed to the px function using dot-notation. The two values are of course equal.

The arithmetic operators work on values with units in the "natural" way, so you can add and subtract values with like units. (You can't just add a number to a unit value, though; 42in + 1 will generate a run-time error.) What's more, if the units are different but have the same underlying dimension (eg both lengths), then they can still be added and subtracted, by converting them to a common scale.

You can multiply values with units by numbers, in the obvious way. You can multiply values with units together, too; 6in * 7in is 42 in in, or 42 square inches. Division works in the corresponding inverse way.

More to follow when I've cleared up the details.

18 enumerations

Sometimes you want a collection of distinct named values, for example as the names of options (small, medium, large, jumbo). Rather than using const integer variables (which can be confused with plain integer values) or strings/symbols (vulnerable to silent mis-spellings), Spice allows you to define enumeration values:

define enum Size = small, medium, large, jumbo enddefine

Size becomes a new type name, and small, medium, large, and jumbo become new values of that type. print(medium) will print medium; the values retain their names. Each value also has an associated small integer, starting at 1 (for the first) and taking on successive values; you can see this number using magnitude, so large.magnitude.println will print 3, and you can construct enum values from numbers: new Size(4) is identical to jumbo. Using values outside the range will generate an error.

Spice's enumerations are just shorthand. An enum type is a class extending the built-in Enum class, which has slots for the name and magnitude of its values. Each enum value is an instance of its class with the slots set appropriately.

Spice implementors are encouraged to implement enum values as efficiently as possible, taking advantage of their specialised nature.

19 symbols and tables

[to be done]

20 all sorts of unmentionable things

20.1 extended relational expressions

[a <= b <= c "works" and has the expected meaning.]

20.2 op

[op Operator and op class; turning operators into their corresponding procedures; noting that op && etc don't evaluate their arguments specially.]

20.3 super and extends

[invoking the next most general procedure definition on an overloaded procedure; specifying same using extends.]

20.4 shared slots

[shared x:T in a class makes a slot shared by all instances of the class.]

20.5 properties

[property p:T in a class makes a property which is copy-on-write in the instances of that class.]

20.6 exceptions

[throw E and catching exceptions with try-catch-endtry]

20.7 memo-expressions

[ensuring that clean up gets done using memo E]

20.8 parallel iteration

[iterating over several things at once with for x in A, y in B]

21 the standard library

Spice has a large library of standard values and procedures. They are described in the manual.