Quick introduction to Converge
This document is intended to give programmers familiar with other programming languages a quick introduction to Converge, essentially pointing out those things that may be different from other languages; it is not intended to be a complete, thorough reference manual. After reading this document you will probably want to read the modules reference manual which documents the standard library, or the guide to compile-time meta-programming which documents Converge's macro-like facilities.
Concrete syntax
Since the direct interaction with Converge is through its concrete syntax in the form of source code in a text editor, let us establish the general rules for representing Converge code. Converge is syntactically similar to Python. This means it relies on indentation to show code blocks rather than using curly brackets { ... } or begin ... end keywords. All indentation in Converge must be in the form of spaces; no other form of whitespace is acceptable. Conventionally 4 space characters form one level of indentation; however you may use any number of space characters to indicate indentation provided you are consistent within your own code.
Hello world!
Let's start with the traditional Hello world! program:
import Sys
func main():
Sys::println("Hello world!")
As expected, when this program is run it prints out Hello world! .
Note how the body of the main function is indented from its func main(): definition. The name of this function is also significant. When a program is run, Converge looks for a function called main and executes that function. If no such function exists, an exception is thrown. Sys is a built-in module. To lookup module definitions such as the println function one uses the :: operator.
Running programs
Converge programs are compiled to bytecode, and the Converge Virtual Machine (VM) executes this bytecode. Converge uses a traditional compiling and linking scheme similar to many C compilers, where individual source files are converted into object files and linked to produce a Converge executable. Large projects may find it convenient to make use of the documentation of the individual compiler tools. However most simpler projects can make use of Converge's auto-make functionality. If the code fragment from the previous section is placed into a file hello.cv , it can be compiled and run automatically as follows:
$ converge hello.cv
Hello world!
$
In other words, when the VM is passed a source script, it automatically compiles dependencies, links, and executes the file. It also caches the results of the compilation so that subsequent runs are much quicker. It is often instructive to see what steps are being taken prior to the file being run. The -v switch to the VM shows the auto-make steps in operation:
$ converge -v hello.cv
===> Compiling /tmp/hello.cv...
===> Finished compiling /tmp/hello.cv.
===> Linking.
Hello world!
$
Converge's auto-make facility ensures that all files in a project are upto date; its chief limitation is that it sometimes has to compile individual files multiple times to ensure that this is the case, even when a user may know that this is not strictly necessary. In such cases, it may be more efficient to use the individual compiler tools.
If you encounter strange compilation problems (perhaps after moving files from one machine to another), converge can be instructed via the -f switch to ignore all your cached bytecode files and perform compilation from scratch, updating all cached bytecode files. Although this is rarely needed, it can be useful on occasion.
Functions and scoping
Function arguments
Functions can accept zero or more arguments in the standard way. The general form of a function is thus:
func m(ai, ..., an, aj := <default value>j;, ... am := <default value>m, *v)
where i, j >= 0 and v is optional. Put another way, this means that (optionally) normal arguments are (optionally) followed by arguments with default values and (optionally) completed by a final 'var args' variable.
The caller of a function must specify values for all normal arguments. Arguments with default values for which no value is specified by the caller will have their default value evaluated and assigned to that arg. All remaining arguments are put into a list and assigned to v .
For example a printf like function would be specified as follows:
func printf(format, *var_args):
...
An fprintf style function which defaults to using standard out might look as follows:
func fprintf(format, stream := Sys::stdout, *var_args):
...
Default values are evaluated each time a value is not passed to a particular argument. This is substantially different than the mechanism found in Python. Note that Converge has no concept of function overloading based on e.g. function arity.
Lexical scoping
Converge is lexically scoped. The following program will thus print 0 then 1 then 1 .
func main():
x := 0
Sys::println(x)
func f2():
Sys::println(x)
x := 1
f2()
Sys::println(x)
Assignment of a variable in a block causes that variable to be made local to the block. The following program will thus print 0 then 0 then 0 .
func main():
x := 0
Sys::println(x)
func f2():
x := 1
Sys::println(x)
f2()
Sys::println(x)
In order to assign to a variable in an outer block, the nonlocal statement at the beginning of a function punches a hole up to an appropriate layer. The following program will thus print 0 then 0 then 1 .
func main():
x := 0
Sys::println(x)
func f2():
nonlocal x
x := 1
Sys::println(x)
f2()
Sys::println(x)
Note that class elements are deliberately excluded from the standard scoping mechanism so that the following is invalid:
class Dog:
type := "Dog"
func get_type(self):
return type
As the above suggests, the first parameter of a bound function will always be set to the self object. Conventionally this parameter is always called self . Via this variable the current objects slots can be accessed:
class Dog:
type := "Dog"
func get_type(self):
return self.type
Functions as lambdas
Converge does not provide any special type of a function for small functions such as a specialised lambda form. However sometimes the indentation syntax of Converge can be inconvenient when passing functions containing only a few expressions as arguments to another function. In such cases one can use the alternative curly bracket syntax as follows:
Functional::map(func (x) { return x.to_str() }, list)
Objects and Classes
Objects
Converge is, at the lowest level, a prototype based object orientated system: everything is an object and every part of an object can be altered at will. Objects are said to consist of slots, each of which has a name and a value. Slots are accessed via the . (dot) operator.
Classes
While the conceptually simple prototype based view of the world has many advantages, classes are provided as an important convenience. As this might suggest, classes are not a base feature of the Converge VM which understands only objects; however for practical reasons Converge provides syntax to specify classes, and objects are generally created via class instantiation.
A simple definition of a class is as follows:
class Animal:
func init(self, name):
self.name := name
func get_name(self):
return self.name
func set_name(self, name):
self.name := name
New objects can be created via the new slot of a class:
fido := Animal.new()
The resulting object can then have its slots accessed and assigned in the standard fashion:
fido.set_name("Fido")
Sys::println(fido.get_name())
fido.name := "Lido"
When an object is first created, its new slot is called to set-up the object; it is common to override the default action provided by new as in the above example.
Functions within a class are slightly different to normal top-level functions; when they are extracted from an object they are bound to that object. The distinguished variable self is automatically assigned to the bound object. In all other respects bound and unbound functions are identical.
Subclasses
A class can specify that it has one or more superclasses by specifying the superclass(es) name(s) as follows:
class Dog(Animal):
...
If more than one superclass is specified, then any naming conflicts between two classes are resolved in favour of the class specified later in the superclass list.
Types
As well as specifying a reusable unit of code, classes also specify types that can be used to classify objects. Classes provide two important functions which can check whether a given object has a relationship to it. C.instantiated(o) succeeds if o was created by C 's new function. C.conformed_by(o) succeeds if o has a matching slot for every slot defined in C (note that o may have been created by a different class but still conform to C ).
Modules
File layout
The conventional structure of a Converge module is imports followed by constant definitions, classes and functions. As in Python, all parts of a module are executed when it is imported. Thus ordering of elements within the file is often important.
For example whilst the following module is correct:
class Animal:
...
class Dog(Animal):
...
reversing the two classes would be incorrect because the creation of Dog would attempt to access the Animal variable before it has had a class assigned to it.
However notice that it is only at the top-level that the ordering of elements is important. For example the following module is perfectly valid because by the time the new_dog function can be called lexical scoping will have ensured that Dog will have been assigned a value:
func new_dog():
return Dog()
class Animal:
...
class Dog(Animal):
...
Accessing module definitions
Until this point we have used the :: operator to access definitions within a foreign module. It should be noted that modules are also normal objects and that a modules' normal slots can be accessed via the standard . (dot) operator as any other object.
Packages and importing
Converge programs are often organized into packages. Packages are directories which are collections of modules and sub-packages. Packages can be nested to an arbitrary depth.
Importing modules
The import statement imports an element into a modules namespace. By default the imported element is assigned to a variable which has the name of the final part of the import statement e.g. import A::B::C will bring into existence a variable C and assigns to it the value of the element A::B::C . This behaviour can be altered by using the as X suffix. e.g. import A::B::C as D will bring into existence a variable D and assigns to it the value of the element A::B::C .
Converge needs to be able to determine at compile-time exactly which modules are imported by the import statement. To facilitate this, the variable created by an import statement can not be assigned to. In other words this is illegal:
import A::B::C as D
...
D := 4
Standard types
Converge provides standard types such as lists, dictionaries, sets and strings:
list := [1, 2, 3]
set := Set{3, 2, 1}
dict := Dict{"a" : 1, "c" : 3, "b" : 2}
str := "123"
Lists
Lists support the full slice notation:
list := [1, 2, 3, 4]
Sys::println(list[1]) // prints 1
Sys::println(list[0 : 2]) // prints [1, 2]
Sys::println(list[-1]) // prints 4
Sys::println(list[1 : -1]) // prints [2, 3]
list[3] := 10
Sys::println(list[-2]) // prints 10
list[1 : -1] := [5, 6, 7]
Sys::println(list) // prints[1, 5, 6, 7, 4]
Indexes start from zero. Negative indexes count n elements from the end of the list.
Sets
Sets do not support the slice notation.
Dictionaries
Dictionaries support some of the slice notation:
dict := Dict{"a" : 1, "c" : 3, "b" : 2}
Sys::println(dict["a"]) // prints 1
dict["b"] := 10
Sys::println(dict["b"]) // prints 10
Strings
Converge strings are immutable and thus only support the lookup aspects of the slice notation:
str := "1234"
Sys::println(str[1]) // prints "1"
Sys::println(str[0 : 2]) // prints "12"
Sys::println(str[-1]) // prints "4"
Sys::println(str[1 : -1]) // prints "23"
Integers
Converge integers are immutable.
Expressions
At its core, Converge is an expression based language based on Icon. As has been seen earlier in the document, simple statements work in Converge much as one would expect. This section mostly documents deviations from the expected norm.
Assignment
Assignment is performed via the := operator to differentiate it from the equality operator == . Assignment returns the value of its right hand side. The following fragment prints 10 :
Sys::println(i := 10)
Syntactic sugar is available for addition += , multiplication *= and division /= e.g.:
i += 2
The above is pure syntactic sugar for:
i := i + 2
Since assignment is an expression, assignments can be chained. The following fragment assigns 10 to both x and y :
x := y := 10
Assignment can be used to unpack sequence types. The following fragment prints 10 then 20 :
x, y := [10, 20]
Sys::println(x)
Sys::println(y)
The number of unpacking variables must equal exactly the size of the list being unpacked or a run-time exception will be raised.
Success and failure
Converge does not have built-in boolean logic, since it provides alternative ways of achieving the expected observable behaviour of boolean logic.
Expressions in Converge can succeed or fail. When an expression succeeds it produces a value; when it fails, various outcomes are possible. A simple example of an expression that succeeds is:
x := 5 < 10
This is in fact two expressions. Firstly the expression 5 < 10 succeeds and produces the value 10. The assignment of 10 to x succeeds and similarly produces the value 10 . In the presence of an if statement the following results in the expected behaviour, printing Correct :
if 5 < 10 then:
Sys::println("Correct")
else:
Sys::println("Should never get here")
If the expression in the if fails then the following fragment similarly produces the expected behaviour:
if 10 < 5 then:
Sys::println("Should never get here.")
else:
Sys::println("Correct.")
Essentially, the comparison 10 < 5 fails, which causes control to branch immediately to the else branch of the if statement.
Generators
Expressions in Converge can sometimes generate multiple values. In such a case, the failure of an expression is not necessarily immediately; instead the generator can be resumed to see if the new value it produces causes the expression to succeed. When all of a generators values are exhausted with no match, then the overall expression fails.
Generators are generally used with the for statement which will pump a generator for values until it produces no more. The following fragment prints all elements in the list l :
l := [1, 3, 5, 8]
for e := l.iter():
Sys::println(e)
Functions can be made to produce multiple values (rather than simply return 'ing a value) with the yield expression. For example the following function will generate all values from m up to but excluding n :
func range(m, n):
while m < n:
yield m
m += 1
fail
The fail statement causes the function to both return and to transmit failure to its caller.
Boolean and and or
Converge's conjunction & operator can be used as the traditional and operator and the alternation operator | as the traditional or operator.
The following condition succeeds only if both comparisons succeed:
if i < j & i > 100:
...
Similarly the following succeeds if either the first comparison succeeds, if it is not, the second comparison succeeds (note that this evaluation is, as expected, lazy so that if the first comparison succeeds the second is not evaluated):
if i < j | i > 100:
...
Conjunction and alternation
The conjunction and alternation operators are in fact part of the general machinery surrounding generators and can be used to control backtracking. It is important to note that control does not back up to arbitrary levels; in fact, backtracking is limited to bound expressions. For example, the condition of an if statement is a bound expression, so the failure of the condition does not cause control to escape back beyond the if statement. Significantly, each separate line in a source file is automatically a bound expression, so if the expression on one line fails, control does not backtrack to previous lines.
Alternation is a special kind of generator which successively generates each of its values. For example, the following prints 1 then 2 :
for i := 1 | 2:
Sys::println(i)
Binary operators
Comparisons (equality, greater than etc.) and other binary operators (add, divide etc.) are defined by functions within objects. The functions' names are the same as the standard infix operators == , + etc. The infix operators are simply syntactic sugar for calling these functions, and thus one can redefine their meaning by providing different definitions of the infix functions within objects. As well as the standard mathematic binary operators, the is operator can be used to test two objects for identity equality.
Control structures
Converge contains a standard complement of control structures.
The if control structure
if <condition> then:
...
elif <condition> then:
...
...
else:
...
Zero or more elif blocks can be specified. A maximum of one else block can be specified.
The ndif control structure
ndif <condition> then:
...
elif <condition> then:
...
...
Zero or more elif blocks can be specified.
ndif is the no default if structure; in other words, a run-time exception is raised if none of the clauses' conditions succeeds. Since the default action is therefore to raise an exception, ndif statements can not contain an else clause.
The for control structure
for evaluates expression and, if it is a generator, pumps it until it fails. If the loop terminates naturally through the exhaustion of expression , the optional exhausted block is executed. If the loop is terminated through a break command, the optional broken block is executed. Both exhausted and broken blocks may optionally be specified on a for structure.
for <expression>:
...
exhausted:
...
broken:
...
The while control structure
while repeatedly evaluates expression until it fails. If the loop terminates naturally through expression failing, the optional exhausted block is executed. If the loop is terminated through a break command, the optional broken block is executed. Both exhausted and broken blocks may be specified on a while structure.
while <expression>:
...
exhausted:
...
broken:
...
Note that while differs from for in that expression is reevaluated on each loop. This means that the following fragment will loop infinitely printing 1 :
l := [1, 2, 3, 4]
while i := l.iter():
Sys::println(i)
since the iter generator will be recreated anew on each loop which is probably not what was intended.
break and continue
break causes the innermost loop to be terminated immediately. continue causes the innermost loop to attempt its next iteration immediately.
It is illegal to use break or continue outside of loops.
try ... catch
Evaluate a block, and catch exceptions that occur during its evaluation:
try:
...
catch <Exception1> to <v1>:
...
catch to <vn>:
...
One or more catch 's must be present. Zero or more catch 's catching specific exceptions may be specified. The final catch may optionally not catch a specific exception at which point it will catch all exceptions that occur.
More on classes
The class hierarchy
Converge follows an ObjVLisp style of classes, metaclasses and objects. Every object is, by convention, an instance of a particular class. Object is the base class from which all other classes ultimately extend. If a class does not specify a superclass, then Object is automatically made its one and only superclass.
Meta-object protocol
An objects behaviour with respect to accessing and setting slots can be controlled via these functions whose default behaviour is as follows:
find_slot(name)
|
Returns the value of the slot name if it exists. Fails if name name does not exist.
|
get_slot(name)
|
Returns the value of the slot name . Throws an exception if name does not exist.
|
set_slot(name, value)
|
Sets the value of the slot name to value .
|
Classes and metaclasses
All classes are instances of Class or one of its subclasses. Thus the classes we have seen earlier in this document have elided the metaclass which defaults to Class :
class Animal metaclass Class:
...
New metaclasses can be defined and used thus:
class Singleton(Class):
func new(self):
if not self.find_slot("instance"):
self.instance := exbi Class.new()
return self.instance
class M metaclass Singleton:
...
Sub-classes of Class generally need to override the new method which is invoked when a class is instantiated, and is expected to return a new object.
Memory management
Converge supports full garbage collection. Users do not need to concern themselves with trivial memory management issues, although as with any such system one should be aware of inadvertently creating long-lived memory cycles which may mean that memory can not be garbage collected.
Compile-time meta-programming
Compile-time meta-programming allows the programmer to interact with the compiler at compile-time and perform tasks such as code generation. A separate document describes compile-time meta-programming.
|