Introduction to Alore
Introduction
Alore is an object-oriented general-purpose programming language with an optional static type system. This document is a short introduction to Alore for a reader who already understands the basics of programming. Some programming experience with an object-oriented programming language such as Python, Java or C# should be enough to understand this document without difficulty.
This document only covers the dynamically-typed subset of Alore. Knowing this subset is enough to get started and to be a productive Alore programmer, but understanding the type system allows you to use Alore to its full potential. The companion document Introduction to Alore Type System gives an overview of the type system. As this document introduces you to the basic features of Alore, you should at least skim this document before reading the type system introduction.
Contents
- Overview
- Hello, world
- Running Alore programs
- Local variables and the Main function
- Comments
- Statements
- Functions
- Global variables and constants
- Types and objects
- Operators
- Classes
- Modules and scopes
- Exceptions
- The standard library
Overview
Alore is a general-purpose programming language designed to be suitable for many typical programming tasks, ranging from small scripts to large and complex applications. It strives for a balanced middle ground between scripting languages and statically-typed object-oriented programming languages. Alore has a compact syntax, flexible built-in data types, powerful string processing facilities and a quick edit-test cycle like a typical scripting language. Unlike pure dynamic or scripting languages, Alore also has an optional static type system that makes it much easier to develop and maintain large and complex applications in Alore.
This document aims to give a concise overview of programming in Alore. Alore is a small language and easy to master, and an experienced programmer should become productive with Alore in a day or two. To highlight the fact that using and knowing the static type system is optional, this document only discusses the dynamically-typed subset of Alore.
Alore has a design philosophy strongly favoring simplicity, clarity and consistency, while always remaining pragmatic. Alore tries to achieve a good balance between readability and expressive power, resulting in a language that encourages writing clear and compact code that is easy to write, understand and maintain.
Alore borrows ideas from many other programming languages. Python is the main source of influence, and there are many obvious similarities between Alore and Python in both syntax and libraries. Other influential languages include Java, Lua, Strongtalk, Boo, C# and Pascal.
Here is a quick list of some major Alore features:
- clean, readable and concise syntax
- dynamic typing
- optional static typing
- you may declare types of variables; type checking detects many programming errors without running a program
- you can freely mix dynamic and static typing within a program
- you can always bypass type checking and run programs immediately without a slow type checking or compilation step
- type declarations serve as automatically-checked documentation and make maintenance easier
- powerful type system features such as generics and type inference
- simple and consistent semantics that
- simplify learning
- improve run-time efficiency
- make it easy to target new platforms such as the browser or the JVM
- help reasoning about programs, both by humans and by tools
- make the formal specification and analysis of Alore practical
- support for native threads that can take advantage of multi-core processors
- support for multiple platforms (Linux, OS X, Windows, Solaris, FreeBSD)
- everything is an object, including primitive types such as integers and booleans
- classes with inheritance
- flexible and powerful basic types, including arbitrary-precision integers, Unicode strings, growable arrays, tuples and hash-table-based maps
- carefully designed standard library with a high density of useful features, and support for static and dynamic typing
- safety from pointer errors and crashes (all error conditions in Alore code are checked by the virtual machine)
- anonymous functions and lexical closures
- hierarchical modules with separate namespaces
- proper, language-enforced encapsulation both at class and module levels using private members or definitions
- structured and extensible exception handling
- automatic garbage collection (no need to manually free memory)
- accessor methods for member variables (also known as properties)
- operator overloading
- a well-defined API for implementing extension modules in C.
This introduction uses a bottom-up approach: basics are introduced first, and explanations of more advances concepts are built on top of more basic concepts. The next sections present the traditional "Hello world" program in Alore, and instructions for getting your first program up and running. The rest of this document explains different language constructs, with short source code examples for each feature. Experienced programmers can get a quick overview of the syntax of Alore by only looking at the section titles and code examples. Finally, the last section gives a quick introduction to some of the most important Alore standard library modules.
We include quite a few details that we could arguably have omitted for brevity. This is used to emphasize the fact that Alore is not only easy to get started with, but there aren't hidden complex features and trivia that you need to properly master the language.
You may refer to the Alore Language Reference for more detailed explanations after having read this document. The Alore Library Reference explains all the modules and types in the Alore standard library. This introduction contains links to the Library Reference for getting further information on specific topics. You can optionally read Introduction to Alore Type System after you have read this document to learn how to use static typing in Alore programs.
Hello, world
The traditional first program simply displays a message:
Print('Hello, world')
Enter the program in any text editor and save it as "hello.alo". We'll show how to run the program in the next section.
The above example only defines a single statement that calls the Print function to display the familiar message.
Running Alore programs
If you have the Alore interpreter in your PATH, you can then run the program you created in the previous section in the shell or the command prompt:
$ alore hello.alo Hello, world $
If you don't have Alore in your PATH, you need to provide the path to the Alore interpreter explicitly. If you used the default installation directory, you can run the program in the Windows Command prompt like this:
C:\Work>\Alore\alore hello.alo
You may wish to add the Alore interpreter to your PATH in Windows (this assumes that you used the default installation folder C:\Alore):
C:\>set path=%path%;C:\Alore
Alternatively, you can run the program in Windows by double-clicking the program file. For this program this method isn't very useful, since the program only flashes quickly and closes immediately.
Local variables and the Main function
The def keyword defines functions and the var keyword is used to define variables:
def Main() var local var two = 'two' -- Define two local variables. var first, second end
The function Main, if present, is called at the beginning of each Alore program. In the example above, Main accepts no arguments, but another supported variant of Main is explained later.
Variables in Alore are untyped – only values (or objects) have types. By convention, functions and other definitions with a global scope (for example, Main) typically start with a capital letter. Local variables (in the example, local, two, first and second) start with a lower case letter.
Local variables are visible from the location of their definition to the end of the block that contains the definition. Function definitions and other global definitions are always visible in the entire file or module that contains the definition, and they must be defined outside a function.
Variable names may contain letters (a-z, A-Z), digits (0-9) and underscores, but they may not start with a digit.
Comments
Two dashes (--) introduce a comment that extends until the end of a line. If the first line of an Alore source file starts with #!, that line also behaves as a comment. This is frequently useful in Unix-like operating systems, and it can be safely ignored in other operating systems.
Statements
Statements are the basic program building block in an imperative language like Alore. We have already seen two kinds of statement: function call and local variable definition. The sections below introduce other statement types, including the assignment statement, if statement and loops.
All statements are accepted both at the top level as well as in functions. However, we recommended you to place all non-initialization statements into functions such as Main instead of the top level of your program to make your code easier to maintain, unless you are writing a short throwaway script.
In many of the code fragments below, we have omitted the enclosing function definition for brevity. Variable names usually start with a lower case letter to make it clear that we actually define local variables.
Assignment statement and expressions
In the following fragment, the local variable i is initialized with the value 1, and subsequently incremented by 1 (to 2):
var i = 1
i = i + 1
You can shorten the previous assignment statement by using += instead of the assignment operator:
i += 1 -- Increment i by 1
Numeric expressions can be written using ordinary arithmetic notation: + and - for addition and subtraction, * for multiplication, / for division, parentheses for grouping, etc.
i = 2 + 3 * 4 -- 2 + 12 == 14 i = (2 + 3) * 4 -- 5 * 4 == 20 3 / 2 -- 1.5
There are also four additional assignment operators, -=, *=, /= and **= (the power or exponentiation operation). They work in a similar fashion to the addition operation += introduced above:
var x = 6 x -= 2 -- 4 x *= 3 -- 12 x **= 2 -- 144 (12 * 12)
Note that the / operator performs a floating point division. The div operator can be used for integer division, rounding down:
3 div 2 -- 1 -3 div 4 -- -1
In addition to numbers, some operators can be applied to other types of values, such as strings ('==' means equality):
'foo' + 'bar' == 'foobar' 3 * 'a' == 'aaa'
The table below introduces some additional operators. Operator precedence is described later in section Operators.
Operators | Description | Example |
---|---|---|
mod | modulus (remainder) | 7 mod 3 == 1 |
** | power | 2**100 |
== | equality | 1 + 2 == 3 |
!= | inequality | 'cat' != 'CAT' |
< <= >= > | order comparison | 2.3 < 3 |
and or not | boolean operators | a >= 2 and b < 5 |
The if statement
The if statement supports multiple conditions using elif:
if i < 5 Print('i less than 5') elif i > 10 Print('i greater than 10') else Print('i between 5 and 10') end
The elif and else parts are optional, and there may more than a single elif part. The end keyword always marks the end of the if statement. In the above example, every code block contains only a single statement, but as a general rule, any number of statements can always be used where a single statement is valid.
The i variable is assumed to be defined outside the above fragment. Variables must be defined before they can be used in expressions.
Line breaks
Line breaks are used to separate statements and other syntactic constructs. In the example in the previous section, each statement that calls Print in the body of the if statement must be on a separate line. Alternatively, semicolons can be used whenever newlines can be used, so the beginning of the previous example could have been written equivalently like this:
if i < 5; Print('i smaller than'); elif i > 10
We recommended usually avoiding the use of semicolons for the sake of consistency and readability. Thus they will not be used in this tutorial after this section.
A single logical line can also span several physical lines. A physical line break is always ignored after one of the following lexical items:
, ( [ = . + - * / div mod ** == != < <= > >= in is or and to :
Thus this (somewhat contrived) example contains only a single logical line:
a[
1 + i] = -- Comments are ignored
5 +
6
The for, while, repeat and break statements
Perhaps the simplest type of loop is the for loop over an integer range (other uses for the for loop are explained later):
-- Count 0, 1, ..., 4. for i in 0 to 5 Print(i) end
Note that the range does not include the upper bound (5). The loop below is equivalent to the preceding for loop. The while loop allows controlling repetition with any boolean expression:
-- Count from 0 to 4 using a while loop. var i = 0 while i < 5 Print(i) i += 1 end
If the while loop condition is false during the first iteration of the loop, the body will not be executed at all. The repeat-until loop is always executed at least once, until the end condition is true:
var reply repeat reply = ReadLn() until reply == 'yes' or reply == 'no'
A while loop with a True condition loops indefinitely. A break statement can be used to leave the loop and continue execution after the end of the loop. The break statement can be used to leave for, while and repeat loops.
var reply while True reply = ReadLn() if reply == 'yes' or reply == 'no' break end WriteLn('Type "yes" or "no".') end -- At this point, reply is either "yes" or "no".
Note that we used WriteLn instead of Print in the above example to display output. WriteLn is preferable to Print in interactive scripts that use ReadLn, since Alore implementations are free to redirect the output of Print to a different window, for example, but ReadLn and WriteLn always access the standard input and output streams, respectively. Another difference is that given more than one argument, Print inserts spaces between its arguments, but WriteLn never automatically inserts any spaces. We use WriteLn instead of Print in the rest of the document.
The switch statement
The switch statement allows selecting from multiple cases depending on the value of an expression:
switch x.lower() case 'yes', 'y' WriteLn('Agree') case 'no', 'n' WriteLn('Disagree') case 'maybe' WriteLn('Ambivalent') else WriteLn('Unknown') end
In the fragment above, the value of the expression x.lower() (string x converted to lower case) is compared against several strings ('yes', 'y', 'no', 'n' and 'maybe') and if one of them matches, the related block of code is run. If none of the cases match, the else block is executed. The else block is optional: if it is missing, the statement behaves as if it was empty.
Each case may have one or more alternatives separated by commas. Each of the alternatives may be any valid expression. The switch statement cannot directly match against a range of values – an if statement is needed to match values between 1.2 and 2.3, for example.
Functions
The def keyword is used to define functions:
def Main() var name = AskName() Greet(name) end def AskName() WriteLn('What is your name?') return ReadLn() end def Greet(name) WriteLn('Hello, ', name) end
The previous fragment defines three functions: Main, AskName and Greet. The Greet function takes a single argument (name). If a function takes two or more arguments, they have to be separated with commas. The AskName function returns a result using the return statement. If a function exits without an explicit return statement, it returns the value nil implicitly. Using return alone, without a return value, also implicitly returns nil.
Although the previous example does not illustrate it, arguments are always passed by value. The values are references to objects, and objects are not copied when passed as arguments to functions. Although functions return technically at most a single value, multiple values can be returned by returning an array of values (for more information, see section Array).
Anonymous functions
The def keyword can be used without a function name to define an anonymous function within any expression. Anonymous functions may access variables defined in enclosing functions:
def Main() var a = 5 var f = def () a += 1 -- Refer to variable defined in the outer scope return a end WriteLn(f()) -- 6 WriteLn(f()) -- 7 WriteLn(a) -- 7 end
Anonymous functions are first-class objects: they can be passed as arguments to functions, returned from functions, stored in composite objects, etc. The rules for the argument lists of anonymous functions are identical to those of ordinary functions.
Default argument values
This example defines a default value for the argument of the Greet function:
def Main() Greet() -- Display "hello, world" Greet('hi') -- Display "hi, world" end def Greet(greeting = 'hello') WriteLn(greeting, ', world') end
The greeting argument is optional: if it isn't given, the default value is assumed. When calling a function with multiple default argument values, the arguments are filled from left to right and the last arguments with missing values are given the default values. The default value expressions are evaluated each time the function is called.
Functions that take an unbounded number of arguments are introduced later in section Functions with an arbitrary number of arguments.
Global variables and constants
Global variables are defined at the top level of a file:
var Global var One = 1 def Main() Global = 4 WriteLn(One + Global) -- Display 5 end
The const keyword defines a global constant:
const Name = 'Alore'
Constants can be used like variables, but they cannot be assigned a new value after initialization. Unlike variables, constants may not be defined locally in functions.
Types and objects
Understanding the concepts type, object and value, and their relationships is fundamental to Alore programming. Let's start with a few definitions:
- Each variable represents a reference to an object. Constants, function definitions and class definitions (described later) are similar.
- A reference to an object is also called a value.
- Each object, and thus each value, has a specific type.
- The type of an object cannot be changed, but a variable may refer to objects of different types during its lifetime.
- There is a small set of special types that are called primitive types.
- Primitive types cannot be inherited from and their instances are immutable. Other types can be inherited from, and their instances can be mutable or immutable.
Primitive types include basic types such as integers, strings and floating point numbers. All types described in this section are primitive unless mentioned otherwise.
Types are also called classes. These terms are used more or less interchangeably. New types can be created by using class definitions. This is postponed to section Classes; this section only illustrates how built-in classes can be used.
Each type is either mutable or immutable (constant). Objects of immutable types cannot be modified after they have been created. Integer and string are typical immutable types, and array is a typical mutable type. Assignment copies only values, i.e. references. There may be several references to a single object. A garbage collector frees the resources consumed by unreferenced objects automatically.
Types are objects as well. Types are typically referred to by the names of their class definitions. For example, the Int type refers to integers and the Str type refers to strings.
The type objects and the basic functions described below are included in the std module. It is a special module that is always available without having to import it. It also contains utility functions such as WriteLn and ReadLn that have already been used in several examples.
Int
We have already seen several examples of using integers. Alore integers are arbitrary-precision, and there is no semantic difference between "short" or "long" integers:
2**200 -- 1606938044258990275541962092341162602522202993782792835301376
Integer literals are entered in base 10. The Int object can be used as a function (constructor) to convert values of other types to integers:
Int('-123') -- -123 Int(2.34) -- 2
Integers and strings cannot be mixed in arithmetic operations. The Int constructor is often used to convert Str objects to integers in arithmetic expressions:
1 + '2' -- Error! 1 + Int('2') -- 3
When converting strings to integers, the default base can be overridden:
Int('ff', 16) -- 255 (integer representation of a hexadecimal string)
Float
Float objects represent real numbers. Internally they are 64-bit floating point numbers. Floating point numbers can be mixed with integers in arithmetic expressions, and the results of mixed operations are always Floats. Examples:
3 / 4 -- 0.75 1.5e2 -- 150.0 (scientific notation) 1.5**2 -- 2.25 4**0.5 -- 2.0 1 + 2.0 -- 3.0 (Float, not Int) Float('-1.2') -- -1.2
Str
Strings are sequences of characters. Individual characters of strings can be indexed using square brackets [ ]. There is no separate character type; characters are strings of length 1. The first character of a string has the index 0, and the length of a string can be queried using the length method.
'' -- Empty string var a = 'foobar' a[0] -- 'f' a[3] -- 'b' a.length() -- 6
A string can be within single or double quotes:
"foo" -- Equivalent to 'foo' '"' -- String with a double quote " "'" -- String with a single quote '
Two consecutive quotes are used to represent a single quote in single-quoted string literals (and similarly for double-quoted strings):
'That''s it!' -- String That's it! """quoted""" -- String "quoted" (in quotes)
Strings are immutable:
a[1] = 'x' -- Error (immutable)!
Slices of strings (substrings) can be created by using the : operator in the index. The slice contains characters up to but not including the slice end index.
a[1:3] -- 'oo' (slice)
The start or end index can be omitted when slicing to get a slice that is a prefix or suffix or a string, respectively.
a[:2] -- 'fo' a[1:] -- 'oobar'
Negative indices can be used to access string contents relative to the end of the string (-1 refers to the last character):
a[-1] -- 'r' (the last character of 'foobar') a[-2] -- 'a' a[:-1] -- 'fooba'
Strings support concatenation and repetition (multiplication by integers):
'foo' + 'bar' -- 'foobar' 3 * 'a' == 'aaa' -- True
String objects can be compared lexicographically (case is significant):
'cat' > 'canine' -- True
The in operator can be used for testing whether a string contains a substring:
'bc' in 'abc' -- True 'd' in 'abc' -- False
The Str constructor can be used to convert objects of most types to strings:
Str(13) -- '13' Str(1.2) -- '1.2' Str([1, 2, 3]) -- '[1, 2, 3]' (convert an array to a string) Str(nil) -- 'nil'
Individual characters are represented as strings with only a single character. The Chr function can be used to build strings representing specific character codes, and the Ord function returns the numeric (usually Unicode) value of a character:
Chr(65) -- 'A' Ord('A') -- 65
String objects provide some additional useful methods. Two methods convert strings to upper and lower case characters:
'foo'.upper() -- 'FOO' (convert to upper case) 'Foo'.lower() -- 'foo' (convert to lower case)
The location of the leftmost instance of a substring can be queried using the find method:
'foobar'.find('bar') -- 3 (find the leftmost index of 'bar') 'foobar'.find('BAR') -- -1 (not found)
You can construct string objects based on a format string using the format method. Formatting sequences between { and } are replaced with method arguments converted to strings using the desired formatting:
'{} weighs {0.0} kg'.format('the dog', 13.72) -- 'the dog weighs 13.7 kg' '{10:}'.format('cat') -- ' cat' (right align field) '{-10:}'.format('cat') -- 'cat ' (left align field)
The count method can be used to calculate how many times a substring occurs in a string:
'foo'.count('o') -- 2 'foo'.count('x') -- 0
You can check whether a string contains a prefix or a suffix:
'foobar'.startsWith('foo') -- True ('foo' is a prefix of 'foobar') 'foobar'.endsWith('bar') -- True ('bar' is a suffix of 'foobar')
You can also construct a copy of any string with leading and trailing whitespace characters removed:
' foobar '.strip() -- 'foobar'
String objects also support replacing all instances of a substring with another string:
'foobar'.replace('oo', 'u') -- 'fubar'
The string module contains several additional functions that make working with strings easier. These functions are introduced later in this document.
Alore standard functions assume strings are encoded in Unicode (16-bit character codes), but string objects can be used to represent strings in various encodings, and even arbitrary binary data. In particular, 0-characters can be freely included in strings. The encodings module implements various character encodings and conversions.
Alore source files may be encoded in UTF-8, ASCII or Latin 1. UTF-8 is the default encoding. Any 16-bit Unicode characters can be encoded using UTF-8. This example contains a Russian word:
-- This source file uses the UTF-8 encoding def Main() var s = 'Здравствуйте' end
Note that the set of characters supported by WriteLn and similar functions depends on the current operating system and locale. Displaying non-ASCII characters might be limited to a narrow subset of Unicode. Consider this example:
-- This source file uses the UTF-8 encoding def Main() var euro = '€' WriteLn(Ord(euro)) -- Display 8364. WriteLn(euro) -- Display € or ? depending on locale. -- ? implies that the euro sign is not -- supported by the current locale. end
The euro sign can be displayed properly in, for example, POSIX compliant operating systems using a UTF-8 locale. This is the default in many Linux distributions.
An encoding declaration at the top of a file can be used to specify a different encoding than UTF-8. This example uses Latin 1 and converts a string to an ASCII representation using the Repr function:
encoding latin1 def Main() var s = Repr('Buenos diás') -- Encode the string using ASCII -- characters to make it displayable WriteLn(s) -- Display 'Buenos di\u00e1s' (00e1 is the -- hexadecimal character code for á) end
Arbitrary character codes can also be directly included in string literals using the \uNNNN construct, even if the file encoding is not UTF-8. Note that backslash \ has no other special meaning and does not need to be escaped in string literals.
'\u1230' -- Character code 4656 (hexadecimal 1230) '\u000a' -- Character code 10 (LF)
Array
Arrays are composite objects that store sequences of references to objects. The contents and the length of an array object can be modified after creation. Array is not a primitive type.
Arrays with a specific length and contents can be built by separating the items in the array with commas, and enclosing the list in square brackets:
[2, 6 + 2, 5] -- Create array with 3 Int items ['foo'] -- Create an array with a single Str item [] -- Create an empty array
Arrays can appear within arrays and argument lists:
a = [1, [2, 3], 4] -- Construct an array with 3 items: Int, Array and Int Fun(1, 2, [3, 4]) -- Call function with 3 arguments: Int, Int and Array
Arrays with a run-time computed length can be created with the multiplication operator *:
a = [0] * 100 -- Array with 100 items, initialized to 0 b = ['x', 'y'] * 2 -- Array ['x', 'y', 'x', 'y']
Length of an array can be queried with the length method:
a.length() -- 100
The indexing operator [ ] can be used to get and set array items. The index of the first item in an array is 0:
a[0] = 'foo' -- Set the first array item a[1] = a[0] + 'bar' -- Get and set array item a[0:2] -- ['foo', 'foobar'] b[2] -- 'x'
Like strings, negative indices refer to array items relative to the end of the array, and arrays support slicing:
a = [1, 2, 3, 4] a[-1] -- 4 (the last item) a[1:-1] -- [2, 3]
The append, insertAt and remove methods can be used to add or delete array items:
a = ['A', 'B', 'C'] a.append('x') -- Add item to end: a == ['A', 'B', 'C', 'x'] a.remove('B') -- Remove all 'B' items: a == ['A', 'C', 'x'] a.insertAt(2, nil) -- Add item: a == ['A', 'C', nil, 'x']
The removeAt method removes an item at the specified index and returns it:
a = [1, 2, 3]
a.removeAt(-1) -- Result 3; now a == [1, 2]
Arrays can compared for equality and lexicographic order:
[1, 2] == [1, 2] -- True [1, 2] < [1, 3, 1] -- True [1, 3] < [1, 2, 1] -- False
Arrays can store references to any objects and a single array can store references to objects of several different types:
a = [1, 'foo', [2, 3]]
Similar to strings, the in operator can be used with arrays for testing whether an object is contained within an array. Likewise, arrays also support the index and count methods:
if 'foo' in a WriteLn('a contains the string "foo"') end a.index('foo') -- 1 (index of the first 'foo' item) a.count('foo') -- 1 (number of 'foo' items in an array)
You can use the + operator to concatenate arrays:
[1, 2] + [3, 4] -- Result [1, 2, 3, 4]
The extend method can be used to concatenate arrays without creating a new Array object, unlike +. This means that it is often much more efficient than +.
a = [1, 2]
a.extend([3, 4]) -- a becomes [1, 2, 3, 4]
The for statement can be used for iterating over all the elements of an array. The loop variable (i in the example below) receives all the items of the array in sequence, starting from the first item:
-- Print 1, foo and nil for i in [1, 'foo', nil] WriteLn(i) end
You can also build an array containing all the items in an iterable object (an object that supports iteration using a for loop). Just pass the iterable object as an argument to Array:
Array(0 to 4) -- [0, 1, 2, 3]
The assignment statement only modifies references of objects. In the example below, the variables a and b are made to point to the same array object. When the object is modified using one of the references (b), the results can be read using the other reference (a).
var a = [1, 2] var b = a b[0] = 3 WriteLn(a) -- Print 3, 2
String objects provide two very useful methods for splitting strings into arrays and back again:
'foo bar !'.split() -- ['foo', 'bar', '!'] (split at whitespace) 'foo,bar,zar'.split(',') -- ['foo', 'bar', 'zar'] ''.join(['foo', 'bar', '!']) -- 'foobar!' ', '.join(['foo', 'bar']) -- 'foo, bar'
Tuple
Tuples resemble arrays, but they are immutable (their items cannot be changed after creation) and they have a fixed length. Use commas to create tuples (without the surrounding square brackets):
var t = 1, 'x' -- Tuple with items 1 and 'x' t[0] -- The first item in a tuple (1) t[1] = 'y' -- Error (immutable)
Empty and single-item tuples have special syntax:
var t0 = () -- Empty tuple var t1 = (2,) -- Single-item tuple
Tuples support iteration and the in operator:
for n in 1, 1, 2, 3, 5 WriteLn(n) -- Write 1, 1, 2, 3 and 5 end if a in (1, 2) -- a is either 1 or 2 end
Tuples and arrays can also be used in the left hand side of an assignment and initialization. This is sometimes called multiple assignment:
var a, b = 1, 2 -- a gets 1, b gets 2 a, b = b, a -- Swap the values of a and b var xy = ['x', 3] a, b = xy -- a gets 'x', b gets 3
If the for loop has multiple index variables, the iterated items are expanded, similar to multiple assignment:
for x, y in (1, 2), (3, 4) -- Expand items in for loop WriteLn(x, '/', y) -- Write 1/2 and 3/4 end
Tuples are often used to return multiple values from functions: a tuple (or array) of values, potentially with mixed types, is returned, and the caller expands the result by assigning it to a tuple of lvalue destinations:
a, b = MultiFunc() -- Expand multiple values returned by a function
Unlike arrays, tuples do not support concatenation or multiplication. Tuples are fixed-length sequences.
Functions with an arbitrary number of arguments
Functions may also accept an arbitrary number of arguments as an array, and the arguments passed to a function can be given in an array or a tuple:
def Main() WriteLines('first', 'second', 'third') var a = ['first', 'second', 'third'] WriteLines(*a) -- Equivalent to the previous call end def WriteLines(*lines) for l in lines WriteLn(l) end end
The program above displays twice the lines "first", "second" and "third" without the quotes. The function WriteLines accepts any number of arguments that will be stored as an array in the lines parameter, as indicated by the asterisk (*) before the name of the parameter.
In a similar fashion, a variable number of arguments can be passed to a function using an array or a tuple, as seen on the last line of the Main function. The asterisk at the caller does not have to match that in the callee; the example below is valid:
a = ['second', 'third'] WriteLines('first', *a)
The asterisk can be mixed with default arguments and ordinary, fixed arguments. The caller of a function does not need to know which arguments have default values and which will be stored as an array – only the valid number of arguments and the valid values for arguments are relevant.
Command line arguments
The Main function of the main Alore source file optionally receives the command line arguments passed to the program. The arguments are provided as an array of strings:
def Main(args) -- args is an array of command line arguments. for arg in args WriteLn(arg) -- Display all arguments end end
The command line arguments are also available as the constant sys::Args.
Map and Pair
While Array objects only support indexing with integers, the Map type allows indexing with values of almost any type:
var m = Map('John': 34, 'Mary': 24) m['John'] -- 34 (value associated with key 'John') m['Jane'] = 50 -- Assign value m['Peter'] -- Error! (invalid key) m.hasKey('Jane') -- True m[1, 'x'] = [2, 3] -- Tuples can be used as keys
A Map object can store an arbitrary mapping (a dictionary) between objects, implemented as a hash table. Like Array, Map is not a primitive type. Note that when using mutable objects like arrays as keys, the objects should not be modified after using them as keys, or you risk not being able to retrieve their values later. As tuples are immutable, they are better map keys than arrays.
More examples of using Map:
var m = Map() -- Create an empty Map m[12] = 56 m['x'] = 66 -- Assign values m.keys() -- [12, 'x'] (array of keys in an arbitrary order) m.remove(12) -- Remove a key/value mapping using a key m.values() -- [66] (array of values)
The contents of a map can be iterated using the for loop. Each item in the iteration is a tuple (key, value):
for key, value in m WriteLn(key, ': ', value) end
The arguments given to the map constructor are Pair objects, immutable composite objects that represent exactly two values:
var p = 'John' : 25 -- Create Pair object p.left -- 'John' p.right -- 25
Boolean values
Instances of the Boolean type, True and False (defined in the std module) are the only values valid in boolean contexts, such as if or while conditions. Comparison and logical operators always return boolean values:
1 == 2 -- False 1 == 1 -- True
Symbolic constants
Global uninitialized constants are symbolic constants. These constants are automatically given unique values of type Constant. Symbolic constants can be compared for equality and they can be converted to strings to query their name.
const Foo -- Define symbolic constant 'Foo' def Main() WriteLn(Foo) -- Display Foo end
The nil value
The reserved word nil refers to a special object that represents an uninitialized or an empty value. Variables are given the value nil before they are explicitly assigned any value.
var a a == nil -- True WriteLn(a) -- nil (uninitialized)
Unlike all other values, nil is not an instance of any type visible to the programmer. nil has no other special properties; in particular, it cannot be used as a boolean value.
Additional primitive types
A Range object represents a sequence of integer values. The range includes integers from the start value up to, but not including, the stop value. The to operator creates a range:
var r = 5 to 13 -- Create Range object representing 5, 6, ..., 12 r.start -- 5 r.stop -- 13 5 in r -- True 13 in r -- False
The in operator can be used to check if a value is within a range. Range objects are usually used in for loops to iterate over an integer range:
var a = [1, 5, 3, 7] for i in 0 to a.length() a[i] *= 3 end -- Now a is [3, 15, 9, 21].
Functions are objects that can be called. Like all other objects, they can be stored in variables or composite objects, passed as arguments to functions, etc.
var func = WriteLn -- Functions are objects func('hello, world') -- Call WriteLn indirectly
Types are objects as well. You can use them to construct values of a specific type and to check if a value has a specific type using the is operator:
Int('23') -- 23 (Int object) 23 is Int -- True 23 is Str -- False Int is Type -- True Type is Type -- True WriteLn is Function -- True
Type objects can also be used with the as operator in cast expressions. A cast expression checks if the left operand has a specific type. If successful, the left operand is the result; otherwise the cast expression generates a runtime error:
1 as Int -- 1 'x' + 'y' as Str -- 'xy' 1 as Float -- Error: 1 is Int, not Float
Cast expressions are useful in statically-typed code. An additional variant, cast to dynamic, actually serves no purpose in dynamically-typed code:
1 as dynamic -- 1 (just return the left operand) 'x' as dynamic -- 'x'
Cast expressions in variable initializers and in default argument values must be in parentheses, or otherwise they will be interpreted as type declarations:
var a = x as Int -- Type declaration var a = (x as Int) -- Cast
Operators
All Alore operators are included in the list below, from the highest to the lowest precedence, with each operator on the same line having the same precedence:
- . () [] (member access, function call, indexing)
- **
- - (unary)
- * / div mod
- + - (binary)
- to
- == != < <= >= > in is
- :
- not
- and
- or
- as (cast)
- , (tuple constructor)
All operators except the exponentiation operator ** are left associative, i.e. a + b + c == (a + b) + c, whereas ** is right associative, i.e. a**b**c == a**(b**c).
Classes
Most non-trivial Alore programs implement some user-defined types or classes. These types can inherit from other user-defined types or non-primitive built-in types such as Array.
Class objects can be called to construct objects of the type. All objects that have a specific type are called instances of that type, including objects of primitive types.
Simple structured data
This example illustrates defining a simple class that includes two member variables name and occupation:
-- Definition of the Person class class Person var name var occupation end def Main() var p = Person('John Smith', -- Construct a Person object 'journalist') WriteLn(p.name) -- John Smith p.name = 'Mary Smith' -- Change name WriteLn(p.name) -- Mary Smith WriteLn(p is Person) -- True p.address = '15 Juniper St.' -- Error! (undefined member) end
The Main function constructs a Person object (an instance of the Person class) and accesses the name member variable. Class members are accessed using the dot operator. By default, class objects take one argument for each member variable that does not have an initializer when called (initializers are described later in this section).
The structure of classes and class instances is immutable. It is not possible to add new members to an object or a class after it has been created or defined.
Methods
Classes usually have member functions or methods in addition to member variables. Methods are defined with a syntax similar to ordinary functions, but the definitions are always within class definitions:
class Person var name var occupation def show() WriteLn('Name: ', name) WriteLn('Occupation: ', occupation) end end def Main() var p = Person('John Smith', 'journalist') p.show() -- Print 'Name: John Smith' and 'Occupation: journalist' end
Other members of a class can be accessed within methods like ordinary variables. Methods receive a reference to the object they are associated with as a hidden argument called self, and the self keyword can be used to refer to this object.
Constructors and initialization
Default values for member variables can be defined by including initialization expressions with a syntax similar to initializing local or global variables. When an object is constructed, the initialized members are automatically given the default values.
The create member function acts as the constructor of a class. When the class object is called, the constructor is called and the arguments are passed to the constructor. The examples in the previous sections did not define constructors; in this case, a default constructor will be automatically defined. Initialized members are initialized before calling the constructor.
This example illustrates the use of initialized members and constructors:
class MyList var count = 0 -- Initialized member var array def create(len) -- Constructor array = [nil] * len end def append(item) array[count] = item count += 1 end def get(index) return array[index] end end def Main() var list = MyList(5) list.append('John') list.append('Mary') WriteLn(list.get(0)) -- John WriteLn(list.get(1)) -- Mary end
Note that this example violates encapsulation, since internal state information is visible outside the class. We later improve the code by using private member variables to achieve better encapsulation.
Object identity
Class instances can be compared for equality. By default, this comparison is based on object identity – only references to the same object are equal to each other:
class Person var name end def Main() var p1 = Person('John') var p2 = Person('John') var p3 = p1 WriteLn(p1 == p2) -- False WriteLn(p1 == p1) -- True WriteLn(p1 == p3) -- True end
This behavior can be changed by overloading the == operator, as described later in section Operator overloading.
Constant members
It is possible to define read-only member variables by using the const keyword in a member variable definition:
class Person const name const occupation const numberOfLimbs = 4 end def Main() var p = Person('Tracy', 'ornithologist') p.name = 'Mary' -- Error! end
These member constants cannot be assigned new values after object construction, but constant members of the self object can be modified within the create method.
Private members
Member definitions can be made private by giving the private modifier:
class MyList private var count = 0 -- Private member variable private var array def create(len) array = [nil] * len end def add(item) array[count] = item count += 1 end def get(index) return array[index] end end def Main() var list = MyList(5) list.add('John') list.add('Mary') WriteLn(list.array) -- Error! end
Private members can only be accessed by methods defined in the same class that defines the private members. In addition, only private members of the self object can be accessed, either as "self.member" or simply as "member". For example:
class Example private def member() ... end def method(x) x.member() -- Error! Invalid even if x has type Example or if x == self self.member() -- Ok, since using self member() -- Also ok (implicit self) end end
Inheritance and polymorphism
Each class can inherit from a single another class using the is keyword after specifying the class name. The class after "is" is the superclass of the defined class, which is called a derived class. Derived classes contain all the members of the superclass and any additional members defined in the derived class. This example defines a base class Shape and two derived classes Square and Circle:
import math -- Use the math module (contains the definition of Pi) -- Base class with a default constructor create(centerX, centerY) class Shape const centerX const centerY end -- Derived class class Square is Shape -- Inherit from Shape const width def create(x, y, width) super.create(x, y) -- Call superclass constructor self.width = width -- self refers to the current object end def area() return width**2 end end -- Another derived class class Circle is Shape const radius def create(x, y, radius) super.create(x, y) self.radius = radius end def area() return Pi * radius**2 end end def Main() var shapes = Square(4, 5, 6), Circle(2, 3, 5) for shape in shapes WriteLn(shape.area()) end end
If a class definition does not specify a superclass, it defaults to Object. All classes except Object have a superclass. The Object type is defined in the std module with almost no functionality of its own: Object instances can only compare themselves with another objects using the == and != operators based on object identity. Inheritance from primitive types (Int, Str, etc.) is not supported.
If a derived class defines a member with an identical name as a public member in the superclass, the definition in the derived class overrides the definition in the superclass. The original definitions of members overridden in a derived class can be accessed as super.member. All references to overridden members, even in superclasses, that are not prefixed with the super keyword, refer to the definition in the subclass instead of the original definition.
Members accessed using the dot operator are determined dynamically based on the type of the value. The Main function in the above example calls the area method of two values of different types. Calls such as this are called polymorphic and the objects they function on do not need to have a common ancestor class provided that the methods have compatible calling conventions.
Accessors
Each member variable has two implicit methods associated with it: a getter and a setter. When the value of a member variable is being read or modified, the operation is transparently performed by calling the getter (when reading) or setter (when modifying) methods. Each member variable has a default getter and setter method that simply read and set the value of the physical variable associated with the member – i.e. the member variable behaves like an ordinary variable. Member constants only have the getter method.
These default getters and setters can be overridden in subclasses by specifying special methods as shown in the fragment below:
def variable -- Getter ... end def variable = param -- Setter ... end
The param behaves like a function argument, and it refers to the new value assigned to the member. Its name is not visible outside the setter. The getter must return a value for the member, while the setter never returns a value (other than the implicit nil value).
This example overrides the inherited default getter and setter methods:
class Base var value = nil -- Member variable, default getter and setter end class Monitor is Base def value -- Getter for value WriteLn('read value') return super.value -- Call original getter end def value = new -- Setter for value WriteLn('set value') super.value = new -- Call original setter to set the value end end def Main() var m = Monitor() -- Do not call setter in initialization m.value = 2 -- Display 'set value' WriteLn(m.value) -- Display 'read value' and 2 end
The getters and setters defined in a superclass can be accessed by prefixing the member variable with the super keyword, as shown in the previous example.
It is also possible to define only the getter, or getter and setter, methods for a member, without a corresponding member variable or constant definition, as seen in the example below. These implicit member variables can be used like ordinary member variables.
class Implicit def member -- Member constant whose value is always 5 return 5 end end
Bound methods
Methods themselves are objects. They can be used like ordinary function objects. These objects are called bound methods since they implicitly contain a reference to the object they are related to. Example:
class Printer var message def print() WriteLn(message) end end def Main() var p = Printer('hello, world') var method = p.print method() -- Call the print method, print 'hello, world' end
Special method names
It is possible to enable class instances to be used with some built-in functions and types by defining specific methods whose names begin with an underscore. For example, the Str constructor tries to call the _str method to convert an object to a string:
class MyList ... as defined previously ... def _str() return 'MyList' end end def Main() var list = MyList(4) WriteLn(Str(list)) -- Write 'MyList' end
The Int constructor calls the _int method in a similar fashion to convert an object to an integer. The underscore in these methods names is only a convention and these methods have no special properties. Underscore is conventionally used as the first character in class member names who are public but are supposed to be accessed only in specific places in the code. Other programming languages support other member visibilities in addition to public and private, such as protected, module-local, friend, etc. Instead of supporting complex visibility specification features, Alore groups all of these under a simple informal convention, which essentially says that do not access members with underscores in their names unless you are given permission to do so by the designer of the class.
Method names that start with underscores are also used to implement operations such as addition, subtraction and indexing. This topic is discussed later in the section Operator overloading.
Interfaces and expanding the for loop
An interface defines a set of member names and their behaviors. An example of a simple interface is the _str interface: a class implements this interface if it defines a public _str method that takes no arguments and returns a string representation of the object. Interfaces may be implicit in dynamically-typed Alore code: they are not visible in Alore code, other than potentially in comments. Therefore any class may implement any number of interfaces, but the interfaces may not be visible when observing the class definition. Alternatively, you can define explicit interfaces using the interface construct, and declare that some classes implement the interfaces using the keyword implements. We ignore explicit interfaces for now; refer to Introduction to Alore Type System for a discussion of explicit interfaces.
Objects conforming to the iterable implicit interface can be iterated using for loops. They must define an iterator method that returns an object that conforms to the iterator implicit interface. The iterator interface contains hasNext and next methods. As an example, Array and Range objects provide the iterator method.
We can define the for loop in terms of an equivalent while loop to clarify the iterable and iterator interfaces:
for i in a ... code ... end
can thought as equivalent to
var e = a.iterator() while e.hasNext() const i = e.next() ... code ... end
if the name e is replaced with a name that does not occur anywhere else in the program.
A form of polymorphic function or method accepts all objects that support a specific interface. In the example below, Show is a polymorphic function that displays the contents of any object that implements the iterable interface. The example also illustrates creating an iterable class.
class DownTo private var index private var min def create(max, min) index = max + 1 self.min = min end -- Return iterator. def iterator() return self end -- Are there any items left in the iteration? def hasNext() return index > min end -- Return the next item in the iteration. def next() index -= 1 return index end end -- Show an iterable object. def Show(o) for i in o WriteLn(i) end end def Main() Show(DownTo(5, 1)) -- Print 5, 4, 3, 2 and 1. Show(['a', 'c', 'b']) -- Print a, c and b. end
Operator overloading
Classes may define methods that are called whenever operators such as + or [] are applied to instances of the class. This is called operator overloading. This applies to all types: even primitive types such as Int and Str have methods that implement the basic arithmetic and comparison operations, etc.
In the example below, we define methods that are called when the indexing operator [] is used. There are separate methods for indexed reads and writes. MyArray instances behave a bit like Array objects:
class MyArray private var array def create(length) array = [nil] * length end def _get(index) return array[index] end def _set(index, new) array[index] = new end end def Main() var a = MyArray(5) a[2] = 'John' WriteLn(a[2]) -- John end
Excessive use of operation overloading may result in programs that are confusing and difficult to understand. A good guideline is to overload an operator only if its overloaded behavior strongly resembles the built-in behavior of the operator. The add operator, for example, should be reserved for arithmetic addition and concatenation.
The table below introduces the method signatures of overloadable operators, except for a few operators with special calling conventions that are described later. The operands x, y and z can be replaced with arbitrary expressions (but note that you may have to add parentheses to ensure the correct evaluation order).
Operation | Equivalent method call |
---|---|
x + y | x._add(y) |
x - y | x._sub(y) |
x * y | x._mul(y) |
x / y | x._div(y) |
x div y | x._idiv(y) |
x mod y | x._mod(y) |
x ** y | x._pow(y) |
x == y | x._eq(y) |
x != y | not x._eq(y) |
x < y | x._lt(y) |
x >= y | not x._lt(y) |
x > y | x._gt(y) |
x <= y | not x._gt(y) |
The _add and _mul methods of Int and Float objects call the _add or _mul method, respectively, of the argument if the argument type is not supported by the method. For example, 3 * 'x' is evaluated as 'x'._mul(3) (the result being 'xxx'), since the integer _mul method does not know how to deal with strings.
The in, unary minus, indexing and call operations behave slightly differently from the binary operators in the previous table:
Operation | Equivalent method call | Notes |
---|---|---|
x in y | y._in(x) | Order of operands reversed |
-x | x._neg() | Unary operation, no arguments |
x[y] | x._get(y) | Indexed read |
x[y] = z | x._set(y, z) | Indexed store |
x(y, z) | x._call(y, z) | Any number of arguments may be used |
There are no methods specific to +=, -=, etc. They are mapped to the corresponding methods for ordinary operators such as + or -. Therefore the following three lines of code are equivalent (if x provides _add):
x += y x = x + y x = x._add(y)
Boolean operators and, or, and not cannot be overloaded. The comparison operators and the in operator must return a boolean value (True or False). Only three methods ( _eq, _lt and _gt) are sufficient for evaluating all comparison operations, since the operators are expected to follow the rules listed below:
- (x != y) is equivalent to (not x == y)
- (x >= y) is equivalent to (not x < y)
- (x <= y) is equivalent to (not x > y)
Modules and scopes
Most of the example programs in previous sections have consisted of a single source file that does not use any external modules other than the std module which is always available. Almost all real-world programs import some additional modules using import declarations at the start of a source file, since the std module provides only fairly basic services. This example uses two built-in modules, math and io:
import math, io def Main() var f = File('output.txt', Output) for i in 0 to 5 f.writeLn(i, ' ', Cos(i)) end f.close() end
The File class is provided by the io module and the Cos function by the math module. These definitions cannot be accessed in a Alore source file without including the proper import declarations.
The previous example illustrates the structure of an Alore program: first import declarations, then one or more definitions of functions, global variables, constants or classes (or statements). The above example has a single import declaration that imports two modules and a single function definition, Main. Likewise, each module contains functions, classes, variables and constants related to a specific task or topic grouped together. When a module is imported, its contents are made available in the rest of the source file.
Many Alore programs define some modules of their own. This way a program can be divided into smaller components that are easier to manage and potentially reuse. Modules can optionally be divided into multiple source files.
A module is defined by creating a directory that contains one or more Alore source files. Typically the directory is created below the directory that contains the main Alore source file. The name of the directory must be the same as the name of the module, and each source file in the module must start with a module header. An example program consisting of a single module "message" and the main source file is illustrated below:
main.alo: import message def Main() Show('Hello!') end message/a.alo: module message private const Hili = ' *** ' def Show(msg) WriteLn(Hili, msg.upper(), Hili) end
The main source file imports the module message that is defined in the other source file. The Show function, like all other public functions, variables, constants and classes, can be used in any file that imports the module. All definitions are public unless they are preceded by the keyword private. The constant Hili is an example of a private constant.
Scopes and name collisions
Each module defines a separate scope. Names in different scopes may overlap. If modules foo and bar both define the name Func and only one of the modules is imported in each source file, the correct name Func will always be referenced. But if a file imports both modules, the name must be prefixed by the module name and the scope resolution operator :: (two colons):
import foo import bar def Main() foo::Func() bar::Func() Func() -- Error: Ambiguous! end
The scope resolution operator can be used even if there is no ambiguity to make it clear which module a name belongs to or to avoid future name collisions, i.e. multiple definitions having the same name.
Another example of avoiding a name collision is in the program below:
def Write(s) -- do something with s end def Main() Write('foo') -- Refers to the Write in this module std::Write('foo') -- Refers to the Write in the std module end
The names defined in the current module take precedence over names defined in other modules. All names in a module are equivalent; they are visible in every file of a module, and even before their definition in a file. This means that this example is valid:
def Main() Hello() end def Hello() WriteLn('hello, world') end
Another case of name collision is possible between class members and global names defined in the same module. In this case, the global names can be accessed by prefixing a name only with the scope resolution operator. Since class members usually start with a lower case letter and global names with an upper case letter, this type of name collision is rare.
var Name class Foo var Name def Bar() Name = 1 -- Refers to the member variable ::Name = 2 -- Refers to the global variable end end
Finally, local variables take precedence over member variables or global variables with the same name. Member variables can be accessed using the self.member notation even if there is a local variable with the same name.
Hierarchical modules
Alore supports subdividing modules into submodules. The name of a submodule has the form main_name::sub_name, where main_name is the name of a module. For example, a submodule of the module message could be named message::french. A submodule is located in a subdirectory of the main module, i.e. message/french in the previous example.
Each submodule is a separate module and is not directly related to any main modules. The main modules do not even have to exist, i.e. their directories do not have to contain any source files. When importing a main module, for example import message, the submodules are not imported automatically. They can be imported in a similar way, for example import message::french.
More than a single level of subdivision are supported, for example company::product::core. We recommend that the number of subdivisions should be kept at fairly low levels, since navigating deep directory hierarchies can be tedious. Hierarchical modules can be used to avoid name collisions. For example, instead of defining a module code in a product acme, it might make sense to define the module as acme::code, since it is conceivable that another module named code could be defined in the future, causing ambiguity.
Variables and other names defined in hierarchical modules can be accessed from the files that import them also by specifying the whole module name as a prefix or only a part of it, at least enough to make the name unambiguous. So acme::code::Foo can be accessed as code::Foo if code::Foo is unambiguous. The module name can be shortened only by dropping some of the initial components. Thus acme::Foo is not a valid abbreviation for acme::code::Foo.
Exceptions
Many things can go wrong when a program is run. When creating a file, for example, we may not have a write permission to the directory we want to create the file in, the disk may be full, or the disk may be faulty. These kinds of conditions cause an exception to be raised. The try statement is used to catch exceptions raised in the body of the statement:
import io def Main() try var f = File('file.txt', Output) f.writeLn('hello, world') f.close() except IoError WriteLn('error') end end
In the example above, any of the three lines within the try statement may raise an IoError exception. The except line near the end of the statement catches all of these errors. The last WriteLn statement is executed only if an IoError exception was raised in the body of the try statement.
The innermost enclosing try statement with the matching exception type always catches a raised exception. If no matching try statement can be found in the function that caused the exception to be raised, the function that called the function is consulted, and so on until finally an error message will be displayed if no except block matches the exception type.
Exception types such as IoError are classes that descend from std::Exception. Each except clause catches exceptions of the specified type and all its subclasses. Therefore except std::Exception catches all exceptions. The std:: prefix can be omitted if there are no other variables with the name Exception.
The std module defines several basic exception types. Many of these can be raised in expressions or built-in functions:
1 + 'foo' -- Raise std::TypeError 1 / 0 -- Raise std::ArithmeticError Chr(-1) -- Raise std::ValueError
Defining and raising exceptions
New exception types can be defined by inheriting from another exception type, and the raise statement is used to raise an exception explicitly:
def Main() try ErrorFunction() except e is MyError -- Bind the exception object to a variable WriteLn('caught MyError with message: ', e.message) end end class MyError is Exception -- Define exception class end def ErrorFunction() raise MyError('problem') end
The std::Exception class takes an optional argument, the message. The exception class MyError is defined in the above example to inherit the constructor. The message "problem" is associated with the exception instance. The Main function catches the exception and stores a reference to it in the e variable. The "variable is" part of the except clause is optional.
The finally block
The optional finally block in a try statement is always executed, even if an exception was raised within the block. If an exception was raised and not catched within the try block, the exception will be re-raised after the finally block has been executed, provided that the finally block itself did not raise any exceptions.
The example below writes the text "hello, world" to a file and closes the file afterwards, even if any exceptions were raised. IoError and ResourceError exceptions are caught:
def Main() try var f = File('file.txt', Output) try f.writeLn('hello, world') finally f.close() end except IoError WriteLn('io error') except ResourceError WriteLn('resource error') end end
Both except and finally blocks cannot be mixed within a single try statement. Multiple except blocks can be defined, and they are evaluated in the order of their definition. Only a single except block will be activated for a single exception; after the block has been executed, the execution continues after the try statement.
The standard library
This section gives a short overview of some commonly-used standard library modules. The Alore Library Reference is a complete reference to all the standard library modules.
Type annotations in the library reference
Descriptions of functions, types and variables in the library reference usually include type annotations. For example, here is a partial description of the function std::Chr:
The type annotations in the function header tell us that Chr accepts a single integer argument (as Int), and it returns a string (as Str).
Container types such as Array have a type argument within < and > in annotations. The type argument indicates the type of items. For example, Array<Str> stands for an array of strings. Some types, such as Map, have more than one type argument.
The std module
The std is a special module that is always implicitly imported in each Alore source file. It contains the primitive types, collection types, exceptions, several utility functions and the several constants, including True and False.
The primitive types defined in the std module are Int, Str, Float, Boolean, Function, Type, Constant, Tuple, Pair and Range. The collection types include Array and Map.
Usage of many of the functions in the std module is illustrated below:
Print('num', 4) -- Write objects to standard output (by default), -- separated by spaces and followed by a line break. -- Flush the output stream after each call. Print -- is useful for debugging. WriteLn('num=', 4) -- Write objects to standard output, followed by a line -- break Write('text') -- Write objects to standard output without a line break ReadLn() -- Read a line of text from the standard input Repr('foo') -- "'foo'" (convert to string, works with all types) Chr(65) -- 'A' (parameter is Unicode character code) Ord('A') -- 65 (Unicode character code of a character) Hash(5) -- Calculate a hash value of an object Reversed([1, 2, 3]) -- [3, 2, 1] Reversed(0 to 3) -- [2, 1, 0] Reversed('foo') -- ['o', 'o' 'f'] Min(3, 2) -- 2 (smallest of two values) Max(3, 2) -- 3 (largest of two values) Sort([4, 3, 5, 1]) -- [1, 3, 4, 5] Exit(1) -- Exit the program by raising ExitException
The container types Array and Map were described earlier in this document.
The std module also contains these constants:
Tab -- Tab character ('\u0009') Newline -- Line break CR -- Carriage return ('\u000d') LF -- Line feed ('\u000a')
This class hierarchy contains all the exception classes in the std module:
Exception ValueError TypeError MemberError ArithmeticError IndexError KeyError CastError ResourceError MemoryError RuntimeError IoError InterruptException ExitException
All exception classes are descendants of the std::Exception class.
The io module
The io module contains the File and Stream classes that allow reading and writing of files and similar objects (streams). The simplest way of accessing the contents of a file is enumerating the lines of the file using a for loop:
import io def Main() var f = File('file.txt') -- Open for reading for s in f -- Enumerate over the lines in the file if s.length() < 5 WriteLn(s) -- Display lines shorter than 5 characters end end f.close() -- Close file end
The write and writeLn methods are used to write to streams:
var f = File('out.txt', Output) -- Open for writing f.write('abc', 1) -- Write without a line break 'abc1' f.writeLn('line') -- Write line f.close()
The std::WriteLn function is equivalent to StdOut.writeLn. StdOut, StdIn and StdErr refer to file objects representing the standard output, input and error streams, respectively.
The read, eof and readLn methods provide finer control of reading from files, whereas the readLines methods returns the contents of a stream as an array of lines:
var f = File('file.txt') var s = f.read(5) -- Read 5 characters as a string while not f.eof() -- Copy the rest of the contents to WriteLn(f.readLn()) -- standard output end f.close() var l = StdIn.readLines()
The std::ReadLn function is equivalent to StdIn.readLn.
The io module also defines classes TextFile and TextStream for accessing encoded text streams.
The os module
The os module allows accessing basic operating system services:
Remove('file.txt') -- Remove file or an empty directory Rename('old.txt', 'new.txt') -- Rename file or directory MakeDir('dirname') -- Create directory ChangeDir('../dir') -- Change current directory CurrentDir() -- Get current directory System('dir') -- Execute shell command Join('foo', 'bar') -- 'foo/bar' ListDir('.') -- Read the contents of a directory IsDir('name') -- Does the path refer to a directory IsFile('name') -- Does the path refer to an ordinary file IsLink('name') -- Does the path refer to a symbolic link DirName('foo/bar/file.txt') -- 'foo/bar' BaseName('foo/bar/file.txt') -- 'file.txt' FileExt('foo/bar/file.txt') -- '.txt' GetEnv('PATH') -- Get the value of an environment variable SetEnv('VAR', 'value') -- Change or add an environment variable var s = Stat('file.txt') -- Get file properties s.size, s.isFile, s.isDir, s.isLink, s.modificationTime, s.accessTime, s.isReadable, s.isWritable Sleep(0.2) -- Wait 0.2 seconds
The math module
The math module contains several useful mathematical functions and the constants Pi and E (Euler's number):
Round(1.5) -- 2.0 (round to nearest integer) Floor(1.7) -- 1.0 (round down to nearest integer) Ceil(1.7) -- 2.0 (round up to nearest integer) Trunc(-1.7) -- -1.0 (round towards zero) Sqrt(4) -- 2.0 (square root) Sin(Pi / 2) -- 1.0 Cos(Pi / 2) -- 0.0 Tan(Pi / 4) -- ~1.0 ArcSin(1.0) -- ~1.571 (Pi / 2) Exp(2) -- ~7.39 (E**2) Log(E**2) -- 2
The bitop module
Bitwise operations are defined in the bitop module:
And(12, 6) -- 4 (bitwise and) Or(12, 6) -- 14 (bitwise or) Xor(12, 6) -- 10 (bitwise exclusive or) Neg(12) -- -13 (bitwise complement) Shl(12, 2) -- 48 (shift left) Shr(12, 2) -- 3 (shift right)
The string module
The string module contains utility functions for dealing with string objects:
IntToStr(255, 16) -- 'ff' (255 in hexadecimal) IsWhitespace(' ') -- True IsWhitespace('x') -- False IsLetter('a') -- True IsLetter('3') -- False IsDigit('5') -- True IsWordChar('a') -- True IsWordChar('6') -- True IsWordChar('.') -- False
The reflect module
The reflect module allows dynamically manipulating and querying objects. The Type class is also useful.
TypeOf(1) -- Get type of object (std::Int) GetMember(o, 'x') -- Get member x of object o SetMember(o, 'x', 2) -- Set member x of object o HasMember(o, 'x') -- Does an object have a member?
The random module
You can construct pseudo-random integers and floating point numbers using the random module:
Random(5) -- Random integer in range 0, 1, ..., 4 RandomFloat() -- Random float (>= 0 and < 1) Seed(n) -- Initialize random number generator
Further reading
Refer to the Alore Library Reference for details of the modules described above and information on other Alore modules, such as:
- The re module provides operations for searching and manipulating strings using regular expressions. It supplements the Str type and the string module with more expressive pattern matching operations.
- The encodings module supports encoding and decoding text stored in different encodings, for example UTF-8. It is often useful when processing non-English text.
- The time module contains types and functions for dealing with times and dates.
- Use the thread module to create multi-threaded programs and synchronize the different threads of execution.
- The socket module allows making network connections between computers.