This part of the tutorial explains AmbientTalk as a simple expression language with a flexible syntax which resembles languages like Ruby, Python and Javascript. This section mainly describes the basic features of the language, namely variables, functions, tables (i.e. arrays) and control flow primitives.
As usual, one can define, assign and refer to a variable. Variable definitions are made with the keyword def. Note that AmbientTalk is a dynamically typed language, so variables do not have a type but can contain any value.
In the examples we use the interactive AmbientTalk shell (iat) where the input and output prompt are represented by > and >> respectively.
>def x := 5 >>5 >def y := x + 2 >>7
Variable definitions can include an initialization expression that immediately initializes the variable. Variable assignment is performed by means of the well-known :=
operator (=
is used for mathematical comparison). AmbientTalk supports assignment to multiple variables as a single assignment expression. For this to work, the number of variable names on the left hand side of “:=” must match the number of expressions on the right hand side of “:=”. A typical application of this is to swap the values of two variables more easily:
>[x, y] := [ y, x ] >>[7,5]
As we will explain later, the [y,x]
syntax simply denotes a literal table (a.k.a. an array).
The variable name is used to refer a variable. The variable is evaluated when referenced.
>x >>7
:=
assignment operator, beware of the following syntactic annoyance: the expression a := 1
denotes an assignment to the variable a
, while a:= 1
is misunderstood by the parser as a: = 1
, which is the invocation of a keyworded message named a:
. Keyworded message sends will be explained later on in this chapter. Hence, as a general rule, don't forget to always put a space between the variable name and the :=
operator.
The table is AmbientTalk's native compound data type. It is akin to what other languages call arrays. The main difference is that tables are indexed from 1
up to their length
, while arrays are indexed from 0
up to length-1
. Like with variables, one can define, assign and refer to a table. Table definitions are also formed with the keyword def in the following format:
def t[ <sizeexpression> ] { <initexpression> }
This constructs a table, the size of which is determined by <sizeexpression>
. The content of each slot is the result of evaluating <initexpression>
. This means that <initexpression>
is evaluated for each slot in the table! Tables of e.g. ascending numbers are easily formed:
>def z := 0 >>0 >def table[5] { z := z + 1 } >>[1, 2, 3, 4, 5]
Although there is no special constructor for definition of multidimensional tables, a table entry can contain another table. This is internally stored as a one-dimensional table whose entries are other tables.
>def vowels := ["a", "e", "i", "o", "u"] >>["a", "e", "i", "o", "u"] >table[3] := vowels >>[1, 2, ["a", "e", "i", "o", "u"], 4, 5] >table[3][2] >>"e"
As shown in the definition of the variable vowels
, AmbientTalk provides literal syntax to encode in-line tables. Table assignment and indexation work as usual, but recall that table indices range from 1
up to table.length
. Some more examples of literal tables:
>[ 1, table, "ambientTalk"] >>[1, [1, 2, ["a", "e", "i", "o", "u"], 4, 5], "ambientTalk"]
AmbientTalk provides the splice operator @
to splice tables into surrounding table expressions:
>[1,@[2,3],4] >>[1, 2, 3, 4] >[1, @[2,[3]], [4], @[5], @[], 6] >>[1, 2, [3], [4], 5, 6]
The splicing operator can be also used in the left-hand side of an assignment or definition to separate the head of a table with its rest elements, as shown below.
>def [first, @rest] := [1,2,3,4] >>[1, 2, 3, 4] >rest >>[2, 3, 4]
As mentioned before, there is no special constructor for definition of multidimensional tables, a table entry can contain another table. In what follows we have a closer look to manipulations with multidimensional tables. Consider a multidimensional table which is extensionally defined as follows:
def a := [[1,0,0], [0,1,0], [0,0,1]]; >>[[1, 0, 0], [0, 1, 0], [0, 0, 1]] >a[1][2] >>0 >a[1] >>[1, 0, 0] >(a[1])[2] := 3; >>3 >a >>[[1, 3, 0], [0, 1, 0], [0, 0, 1]]
An implicit definition of the same table can be expressed as follows:
def i := 0; def aux[3] {0}; def b[3] { i := i + 1; aux := [0,0,0]; aux[i] :=1; aux}; >>[[1, 0, 0], [0, 1, 0], [0, 0, 1]]
You can find later in this chapter a helper function for creating matrices here.
Analogous to variables and tables, functions are defined with the keyword def in the form of:
def functionname( <arglist> ) { <body> }
The argument list is just a list of local variables which are always evaluated one by one from left to right. Hence, AmbientTalk employs applicative-order function calls, like Scheme. A basic square
function looks like this:
>def square (x) { x*x } >>nil >square(5) >>25
This example also illustrates the canonical function calling syntax. Calls to functions without parameters must also include the parentheses as shown below.
>def f() { nil } >><closure:f> >f() >>nil
The return value of a function is the result of the last executed statement. Functions always return a value, but a function can always opt to return the nil object.
;
. A syntax error often made in AmbientTalk is to write:
def funA() { // do something useful } def funB() { // do something else }
The parser will complain saying that def
was an unexpected token. The reason is that the function definition statements should be separated by means of ;
. In languages like C and Java, the }
token need not be followed by a semicolon, hence the confusion.
Functions in AmbientTalk are lexically scoped, which means that free variables are looked up in the enclosing environment of the function definition. This is illustrated in the following example:
>def counter := 0 >>0 > def inc() { counter := counter + 1} >><closure:inc> >inc() >>1
Functions can call themselves recusively and they can also be nested in the definitions of other functions such as:
>def fac(n) { def inner(n, result) { if: (n = 0) then: { result } else: { inner( n-1, n * result) } }; inner(n,1) } >><closure:fac> >fac(5) >>120
This example also illustrates how a function can be made “private” by means of lexical scoping rules. Variables and functions defined locally to functions are only visible in the scope of the function where there were defined. Note that the local inner
function is only visible inside the fac
function and its nested scopes.
You can create functions that take an arbitrary number of arguments (also known as a variable arity or polyadic function) by means of the splicing operator @
which collects the actual arguments into a table:
>def sum(@args) { def total := 0; foreach: { |el| total := total + el } in: args; total }; >><closure:sum> >sum(1,2,3) >>6
When the sum function is called, the args table is spliced and passed as the argument list to the function. Note that the args table can also be modified inside the body of the function.
Alternatively, we could define the sum function to take at least two numbers as shown below:
>def sum(a, b, @rest){ { def total := a + b; foreach: { |el| total := total + el } in: rest; total} >><closure:sum> >sum(1,2,3) >>6
In that case, the sum function still accepts an arbitrary number of arguments as long as two arguments are supplied. a and b are considered as mandatory arguments of the argument list.
The splice operator can also be used to transform a table into an argument list for a function, for example:
def args := [3,4,5]; > sum(1,2, @args); >> 15
One way to think about this is that the splice operator splices the args
table into the table of actual arguments. The “rest” arguments do not necessarily need to be the last parameters, for example:
> sum(1,2,@args,6); >> 21
A function can also declare optional arguments as shown below. Optional arguments can be omitted in a function call. If this is the case, the default expression provided in their definition is evaluated and passed as argument to the function instead.
>def incr( number, step := 1){ number + step} >><closure:incr> >incr(3) >>4 >incr(3,3) >>6
As is customary in languages with the above optional arguments, AmbientTalk requires mandatory parameters to be defined before optional parameters, which should in turn be defined before a variable-argument parameter, if any.
Let us show how to use optional arguments to define an auxilary function that creates matrices:
def makeMatrix(n, m := n, init := { |i,j| 0}){ def [i,j] := [0,0]; def makeCol(i,j) { def col[m] { j := j + 1; init(i,j) } }; def matrix[n] { i := i + 1; makeCol(i,j)} }; >def c := makeMatrix(3); >>[[0, 0, 0], [0, 0, 0], [0, 0, 0]] >c[1] := [1,2,3] >>[1, 2, 3] >c >>[[1, 2, 3], [0, 0, 0], [0, 0, 0]] >def d := makeMatrix(4,4, {|i,j| if: (i == j) then: {1} else: {0}}); >> [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
As you have probably noticed in the previous examples, the value returned by a function definition is a closure. Actually in AmbientTalk functions are implemented as named closures.
The function name can be thus used to refer the function (without calling it). This will also return a closure to that function. As an example consider the makeCell function:
>def makeCell(val){ def getter() { val} ; def setter(v) {val := v}; [getter, setter] } >><closure:makeCell> >def [get, set] := makeCell(42); >>[<closure:getter>, <closure:setter>] >get(); >>42
This example also illustrates how a function can make public some of its local fields or functions by returning them as its return value. The get
and set
operations can then be passed separately throughout the application, e.g. an application module that has read-only access to val
only receives the get
closure.
In AmbientTalk, blocks are merely syntactic sugar for the creation of anonymous closures (also known as lambdas). Blocks are creating using curly braces in the form of:
{ |<parlist>| <body> }
If the block does not require any parameter, the |<parlist>| can be omitted. Consider a basic block to sum two numbers:
>{|a, b| a+ b}(3,2) >>5
Note that the argument list passed to the block can define the different types of arguments previously explained.
>def sum := {|a, b, @rest| def total := a + b; foreach: { |el| total := total + el} in: rest; total }; >><closure:lambda> >sum(1,2,3) >>6
This example also illustrates that blocks are also used to iterate over enumerations, such as in foreach: {} in: table.
[ args | body ]
. In AmbientTalk, the <parlist>
is only used to denote parameters to the block, not also for temporary variables as in Smalltalk.
AmbientTalk supports keyworded messages, as in Smalltalk and Self. We have already seen some examples of keyword messages in the previous sections such as the foreach:in:
call. Here is how to define a simple map:onto:
function that takes a closure as input and applies the closure to each element of a table:
>def map: clo onto: tbl { def i := 0; def mapped[tbl.length] { i := i+1; clo( tbl[ i ] ) }; } >> <closure:map:onto:>
It can be invoked as follows:
>map: square onto: [1,2,3] >>[1,4,9]
In AmbientTalk keyworded functions and function calls are actually syntactic sugar. They are transformed by the parser into their canonical equivalent. Hence:
def foo: arg1 bar: arg2 {...}
is transformed into:
def foo:bar:(arg1,arg2){...}
It is also possible to invoke keyworded functions using the canonical function application syntax:
foo:bar:(1,2)
foo: foo: 1 bar: 2 bar: 3
is parsed as foo: (foo: 1 bar: 2 bar: 3)
, not as foo: (foo: 1 bar: 2) bar: 3
. It is recommended to always explicitly parenthesize nested keyworded function calls.
The basic data types in AmbientTalk are numbers (i.e. integers), fractions (i.e. double precision floating point numbers), text (i.e. strings), tables (i.e. arrays) and booleans. In fact, instances of these data types are nothing but objects and as such, they respond to a variety of native methods. Objects will be the subject of the next chapter of the tutorial. This section explains the basic data types and includes some examples how to manipulate them. The complete list of methods can be found in the language reference.
AmbientTalk supports numbers and fractions which represent what other languages call integers and double precision floating point numbers, respectively.
Note that since numerical types are objects in AmbientTalk, the traditional operators +,-,*,/, >, <, <=, >=, =, != are nothing but syntactic sugar for method invocations. Therefore, 1+1
is internally translated into 1.+(1)
. Unary operators are just applications, e.g. -5
is internally translated into -(5)
. What follows are some basic examples of manipulations with numeric types:
>1.inc() >>2 >1.cos() >>0.5403023058681398 >1 ** 5 >>[1, 2, 3, 4] >5 *** 1 >>[5, 4, 3, 2, 1] >1.4567.round() >>1 >1.8.floor() >>1 >1.4.ceiling() >>2
Beware of the precedence rules for function application versus method invocation, which may lead to unexpected results, e.g.:
>-1.abs() >>-1
This code is interpreted as -(1.abs())
, hence the result.
Numbers also support some useful iterator methods such as:
>1.to: 5 do: { |i| system.println(i)} 1 2 3 4 5 >1.to: 5 step: 2 do: { |i| system.println(i)} 1 3 5 >6.downTo: 0 step: 2 do: { |i| system.println(i) } 6 4 2 0 >>nil >3.doTimes: { |i| system.println(i) } 1 2 3 >>nil
A text data type represent a string of characters. Texts are often created using sequences of characters surrounded by double quotes (“). AmbientTalk doesn't use different notation for character or texts so a character can be created as “a”. What follows is some basic examples of some useful native methods supported by text objects:
>"ambienttalk".explode() >>["a", "m", "b", "i", "e", "n", "t", "t", "a", "l", "k"] >"one, two, three".split(",") >>["one", "two", "three"] >"ambienttalk".replace: "[aeiou]" by: { |vowel| vowel.toUpperCase() } >>"AmbIEnttAlk" >"A".toLowerCase() >>"a" >"ambienttalk".length() >>11
AmbientTalk also provides some useful support for pattern matching using regular expressions.
>"ambienttalk" ~= "java" >>false >"ambienttalk" ~= ".*tt.*" >>true
We have already introduced how to define tables. Let us now focus on how to manipulate them with the native methods provided by the table object.
>[1,2,3].filter: {|e| e != 2 } >>[1, 3] >[1,2,3].map: { |i| i + 1 } >>[2, 3, 4] >def vowels := ["a", "e", "i", "o", "u"] >>["a", "e", "i", "o", "u"] >vowels.length >>5 >vowels.at(1) >>"a" >vowels.atPut(1, "z") >>"z" >vowels >>["z", "e", "i", "o", "u"] >vowels.select(2,5).implode() >>"eio" >vowels.isEmpty() >>false
Tables also support some useful iterator methods as shown below.
>def sum:= 0; >>0 >[1,2,3].each: { |i| sum := sum + i } >>nil >sum >>6 >def sumNnum (@args) { args.inject: 0 into: { |total, next| total + next} } >><closure:sumNnum> >sumNnum(1,2,3) >>6
As any native type, booleans are objects so, they respond to keyword messages such as:
<booleanexpr>.ifTrue: { ...} <booleanexpr>.ifFalse: { ...} <booleanexpr>.ifTrue: { ...} ifFalse: {} <booleanexpr>.whileTrue: {...}
=
and !=
are the infix operators for equality and inequality. The prefix operator !
represents logical negation. true
and false
are the prototypical boolean singleton objects. What follows is some basic examples of boolean manipulation:
>(0 < 1).ifTrue: { 0 } >>0 >(3 != 5).ifTrue: { 1 } ifFalse: { 0 } >>1 > def [i, j] := [1,3] >>[1, 3] >{i < j}.whileTrue: { system.println(i); i := i + 1 } 1 2 >>nil
Compound boolean expressions can be created by means of a boolean's and:
and or:
methods, which both take a zero-argument closure as argument. For example, false.and: { 1/0 }
will return false
. The block is not applied because a logical and with false
always fails.
Control flow constructs are defined in the “lexical root”. The lexical root is an object containing globally visible native methods (i.e. it is the top-level environment). We have already seen in the previous sections examples of use of the foreach and if:then:
control structures. A list of traditional control flow structures defined in AmbientTalk is shown below:
if: booleanCondition then: consequentClosure if: booleanCondition then: consequentClosure else: alternativeClosure while: conditionClosure do: body foreach: iteratorClosure in: table do: bodyClosure if: condition do: bodyClosure unless: condition
conditionClosure
in the while:do:
construct denotes a closure that should return a boolean value. It needs to be a closure because the code is evaluated repeatedly until the closure returns false. bodyClosure
, consequentClosure
, alternativeClosure
all denote zero-argument closures. As a general rule, all code that needs to be delayed or executed repeatedly must be wrapped in a closure.
The above definitions in the lexical root of AmbientTalk are simply convenience functions for the methods defined on booleans and closures. For example, an if-statement can also be encoded as a message send, as in Smalltalk: boolean.ifTrue: {…} ifFalse: {…}
.
An example of usage for some of the above structures is shown below in the definition of the sort function.
>def sort(table, cmp := { |e1,e2| e1 < e2 }) { def quickSort(table, low, high) { def left := low; def right := high; def pivot := table[(left+right) /- 2]; def save := nil; while: { left <= right } do: { while: { cmp(table[left], pivot) } do: { left := left + 1 }; while: { cmp(pivot, table[right]) } do: { right := right - 1 }; if: (left <= right) then: { // swap elements save := table[left]; table[left] := table[right]; table[right] := save; left := left + 1; right := right - 1; }; }; if: (low<right) then: { quickSort(table,low,right) }; if: (high>left) then: { quickSort(table,left,high) }; table; }; quickSort(table, 1, table.length); }; >><closure:sort> >sort([2,37,6,4,5,8]) >>[2, 4, 5, 6, 8, 37]
AmbientTalk has no return
statement. To achieve a similar jump in the control flow, see the section on escaping continuations.