The Basics

ObjectTalk is an object-oriented scripting language that was developed from the ground up to be efficient and pure. Nonetheless, many parts of ObjectTalk will be familiar if you have experience with developing in C and derived languages.

ObjectTalk provides its own versions of fundamental C types, including Boolean, Integer (for int), Real (for double), String for textual data and Function as first class primitives. ObjectTalk also provides powerful versions of the three primary collection types: Array, Dictionary and Set. In fact, in ObjectTalk everything is an object derived from a class and the word type is only used internally in the language's runtime. Primitives like Booleans, Integers, Reals, Strings and Functions are objects just like Arrays, Dicts, Sets and HTTP servers.

Variables

Variables associate a name (e.g. myVariable) with an object derived from a certain class (e.g. the number 10 or the string "Hello"). Variables in ObjectTalk must be declared to establish visibility in a certain scope. In the following example (let's assume this is at the top level of a module), the value 10 will be assigned to variable myVariable in the module's scope.

var myVariable = 10;
print(myVariable); // this will print the number 10.

In the following example, the variable is assigned to a class and therefore becomes a class member.

class myClass: Object {
	var myVariable = 10;
}

print(myClass.myVariable); // this will print the number 10.

As you can see from the examples, variables do not have to be type declared as the assignment determines what type of object is stored in the variable.

Comments

In ObjectTalk, you can use comments to include non-executable text in your code or as a note or reminder to yourself. Comments are ignored by the ObjectTalk compiler when your code is compiled. ObjectTalk allows three types of comments: multiline C-style comments, single line C++ style comments and single line shell-style comments.

Multiline C-style comments start with a forward-slash followed by an asterisk:

/* This is also a comment
   written over multiple lines. */

Single-line C++ style comments begin with two forward-slashes:

// This is a comment.

Single-line shell-style comments begin with a number sign:

# This is a comment.

A shell style comment allows ObjectTalk scripts to be made executable in UNIX type systems. The following first line (typically called a "shebang") tells the UNIX shell to execute this script with the ObjectTalk interpreter.

#!/usr/bin/ot

Semicolons

Unlike some newer languages, ObjectTalk mandates the use of Semicolons at the end of statements that don't end with a block. This allows statements to be written over multiple lines (without the need for backslashes) and it avoids possible ambiguity. For example, earlier versions of ObjectTalk allowed:

a = 10
++b

To most people, it is obvious that we want to assign the number 10 to variable a and pre-increment variable b. The compiler however doesn't known the first statement ended after the number 10 and it will generate code for:

a = 10++;
b;

To avoid this confusion, semicolons are mandatory at the end of statements that don't end with a block so the ObjectTalk script should have read:

a = 10;
++b;

Null

Null is a special value that indicates a valueless state. All instances of the Object class are null values. The default global scope defines the variable null as a convenience.

var nothing = null;
var alsoNothing = Object();

Booleans

Booleans in ObjectTalk can only hold two logical values: true and false. The default global scope defines the variables true and false as a convenience. A large number of language operators or member functions return Booleans as a result.

var bool1 = true;
var bool2 = false;
var bool3 = !bool2;

Integers

Integers are signed whole numbers with no fractional component, such as 42, +1 and -23. On most systems, this number is implemented as a 64-bit number meaning that the extremes are -9,223,372,036,854,775,808 and 9,223,372,036,854,775,807 (inclusive).

Integer literals can be written as:

  • A decimal number, with no prefix
  • A binary number, with a 0b or 0B prefix
  • An octal number, with a 0o or 0O prefix
  • A hexadecimal number, with a 0x or 0X prefix

All the following integer literals have a decimal value of 17:

var decimalInteger = 17;
var binaryInteger = 0b10001;       // 17 in binary notation
var octalInteger = 0o21;           // 17 in octal notation
var hexadecimalInteger = 0x11;     // 17 in hexadecimal notation

Reals

Reals are signed floating point numbers with a fractional component, such as 3.14159, 0.1, and -273.15. Reals are implemented using 8 bytes and have a range of 1.7E +/- 308 (15 digits).

Real literals can be written as a floating point number with or without an exponent:

var decimalDouble = 12.1875;
var exponentDouble = -1.21875e1;

The default global language context predefines the variable pi (3.14...) and e (2.71...) as a convenience.

Strings

Strings are a series of characters, such as "hello, world" or "albatross". ObjectTalk strings are instances of the String class. Strings are made up of unicode characters encoded in UTF-8. Given that UTF-8 uses variable length encoding, ObjectTalk does not measure or index strings in bytes but rather in codewords that represent a character whether it is 1,2, 3 or 4 bytes long.

String literals are encoded using the same logic as JSON making it easy to exchange information with other languages or systems. A string literal is a sequence of characters surrounded by double quotation marks (").

var someString = "Some string literal value";
var message = "\tMost Europeans like the \u00C4.\n";

Multiline strings can also be created:

var quotation = "The White Rabbit put on his spectacles.
	'Where shall I begin, please your Majesty?' he asked.

	'Begin at the beginning,' the King said gravely,
	'and go on till you come to the end; then stop.'";

Functions

Functions are self-contained chunks of code that perform a specific task. You give a function a name that identifies what it does, and this name is used to “call” the function to perform its task when needed. See the Functions section below for more details.

function test1(callback) {
	callback();
}

var test2 = function() {
	print("Hello, world!");
};

test1(test2);

Collections

ObjectTalk provides three primary collection types, known as Arrays, Dictionaries and Sets. Arrays are ordered collections of values. Dictionaries are unordered collections of key-value associations. Sets are unordered collections of unique values.

Arrays

An array stores values in an ordered list. The same value can appear in an array multiple times at different positions.

Creating an Array

You can create an array using a literal or the Array class constructor:

var array1 = [];
var array2 = [1, 2, 3.14, "test"];
var array3 = Array();
var array4 = Array(1, 2, 3.14, "test");

Accessing and Modifying an Array

You access and modify an array through its member functions, or by using the index ([]) and addition (+) operators.

var array = [1, 2, 3.14, "test"];

array.append("name");
array.insert(3, "insert");
array += 9;

var size = array.size();

var entry = array[2];
array[0] = 0;

array.erase(2);
array.eraseMultiple(1, 2);
array.clear();

You can merge arrays:

var array1 = [1, 2, 3.14, "test"];
var array2 = [8, 9];

var array3 = array1.merge(array2);
// array3 is now [1, 2, 3.14, "test", 8, 9]

You can use an array as a stack:

var stack = Array();
stack.push(5);
var item = stack.pop();

Iterating over an Array

You can iterate over the entire set of values in an array with the for-in loop:

var array = [1, 2, 3.14, "test"];

for item in array {
	print(item);
}

Sorting an Array

You can sort an array using the sort, rsort and csort member functions. sort puts the array elements in ascending order, rsort is a reverse sort putting the elements in a decending order and csort is a custom sort that allows you to provide a function to determine the order.

var array = [1, 2, 3, 4];

array.rsort();
// array is now [4, 3, 2, 1]

array.sort();
// array is now [1, 2, 3, 4]

array.csort(function(a, b) {
	return a > b;
});

// array is now [4, 3, 2, 1]

Dictionaries

A dictionary stores associations between string keys and values in a collection with no defined ordering. Each value is associated with a unique key, which acts as an identifier for that value within the dictionary. Unlike items in an array, items in a dictionary don’t have a specified order. You use a dictionary when you need to look up values based on their identifier, in much the same way that a real-world dictionary is used to look up the definition for a particular word.

Creating a Dictionary

You can create an Dictionary using a literal or the Dict class constructor:

var dict1 = { "name": "Doe", "age": 29 };
var dict2 = Dict("name", "Smith", "age", 31);

Accessing and Modifying a Dictionary

You access and modify a dictionary through its member functions, or by using index operator ([]).

var dict = Dict();

dict["name"] = "John";
dict["age"] = 39;
dict.erase("age");

var size = dict.size();

dict.clear();

Iterating over a Dictionary

You can iterate over a dictionary's keys or values.

var dict = { "name": "Doe", "age": 29 };

for key in dict.keys() {
	print(key, ": ", dict[key]);
}

for value in dict.values() {
	print(value);
}

Dictionary Membership

Dictionary membership can be determined with the (not) in operators or the contains member functions.

var dict = { "name": "Doe", "age": 29 };

assert("name" in dict);
assert("address" not in dict);
assert(dict.contains("age") == true);

Sets

A set stores distinct values in a collection with no defined ordering. You can use a set instead of an array when the order of items isn’t important, or when you need to ensure that an item only appears once.

Creating a Set

You can create a set with the Set class constructor.

var set1 = Set();
var set2 = Set(1, 5, 9);

Accessing and Modifying a Set

You access and modify a set through its member functions, or by using the addition (+) and subtraction (-) operators.

var set = Set();
set.insert(1);
set += 2;
set.insert(4);
s -= 1;
set.erase(2);

var size = set.size();

set.clear();

Set Operations

You can efficiently perform fundamental set operations, such as combining two sets together, determining which values two sets have in common, or determining whether two sets contain all, some, or none of the same values.

var set = Set(1, 2, 3, 5);
var set2 = Set(1, 3, 6, 8);

assert(set.intersection(set2) == Set(1, 3));
assert(set.difference(set2) == Set(2, 5, 6, 8));
assert(set.union(set2) == Set(1, 2, 3, 5, 6, 8));
assert(set.subtract(set2) == Set(2, 5));

Iterating over a Set

You can iterate over the entire set of values in an array with the for-in loop:

var set = Set(1, 2, 3.14, "test");

for item in set {
	print(item);
}

Set Membership

Set membership can be determined with the (not) in operators or the contains member functions.

var set = Set(1, 2, 3.14, "test");

assert(1 in set);
assert("hello" not in set);
assert(set.contains("test") == true);

Operators

An operator is a special symbol or phrase that you use to check, change, or combine values. For example, the addition operator (+) adds two objects, as in var i = 1 + 2, and the logical AND operator (&&) combines two Boolean values, as in if enteredDoorCode && passedRetinaScan. ObjectTalk supports the operators you may already know from languages like C.

Terminology

Operators are unary, binary, or ternary:

  • Unary operators operate on a single target (such as -a). Unary prefix operators appear immediately before their target (such as !b), and unary postfix operators appear immediately after their target (such as c++).
  • Binary operators operate on two targets (such as 2 + 3) and are infix because they appear in between their two targets.
  • Ternary operators operate on three targets. Like C, ObjectTalk has only one ternary operator, the ternary conditional operator (a ? b : c).

The values that operators affect are operands. In the expression 1 + 2, the + symbol is a binary operator and its two operands are the values 1 and 2.

Assignment Operator

The assignment operator (a = b) initializes or updates the value of a with the value of b:

var b = 10;
var a = 5;
a = b;
// a is now equal to 10

Arithmetic Operators

ObjectTalk supports the four standard arithmetic operators:

  • Addition (+)
  • Subtraction (-)
  • Multiplication (*)
  • Division (/)

Examples:

1 + 2       // equals 3
5 - 3       // equals 2
2 * 3       // equals 6
10.0 / 2.5  // equals 4.0

The addition operator is also supported for String concatenation or for any class that implements the __add__ member function.

"hello, " + "world"  // equals "hello, world"

Remainder Operator

The remainder operator (a % b) works out how many multiples of b will fit inside a and returns the value that’s left over (known as the remainder).

Unary Minus Operator

The sign of a numeric value can be toggled using a prefixed -, known as the unary minus operator:

var three = 3;
var minusThree = -three;       // equals -3
var plusThree = -minusThree;   // equals 3, or "minus minus three"

The unary minus operator (-) is prepended directly before the value it operates on, without any white space.

Unary Plus Operator

The unary plus operator (+) simply returns the value it operates on, without any change:

let minusSix = -6;
let alsoMinusSix = +minusSix;  // equals -6

Although the unary plus operator doesn’t actually do anything, you can use it to provide symmetry in your code for positive numbers when also using the unary minus operator for negative numbers.

Compound Assignment Operators

Like C, ObjectTalk provides compound assignment operators that combine assignment (=) with another operation. One example is the addition assignment operator (+=):

var a = 1;
a += 2;
// a is now equal to 3

The expression a += 2 is shorthand for a = a + 2. Effectively, the addition and the assignment are combined into one operator that performs both tasks at the same time. The generated code for a += 2 however is identical to the code generated for a = a + 2. a += 2 is therefore just "syntactical sugar".

Comparison Operators

ObjectTalk supports the following comparison operators:

  • Equal to (a == b)
  • Not equal to (a != b)
  • Greater than (a > b)
  • Less than (a < b)
  • Greater than or equal to (a >= b)
  • Less than or equal to (a <= b)

Examples:

1 == 1   // true because 1 is equal to 1
2 != 1   // true because 2 isn't equal to 1
2 > 1    // true because 2 is greater than 1
1 < 2    // true because 1 is less than 2
1 >= 1   // true because 1 is greater than or equal to 1
2 <= 1   // false because 2 isn't less than or equal to 1

Comparison operators are often used in conditional statements, such as the if statement:

var name = "world";

if name == "world" {
    print("hello, world")

} else {
    print("I'm sorry ", name, ", but I don't recognize you")
}

// prints "hello, world"

Ternary Conditional Operator

The ternary conditional operator is a special operator with three parts, which takes the form question ? answer1 : answer2. It’s a shortcut for evaluating one of two expressions based on whether question is true or false. If question is true, it evaluates answer1 and returns its value; otherwise, it evaluates answer2 and returns its value. The ternary conditional operator is shorthand for the code below:

if question {
    answer1

} else {
    answer2
}

Here’s an example, which calculates the height for a table row. The row height should be 50 points taller than the content height if the row has a header, and 20 points taller if the row doesn’t have a header:

var contentHeight = 40;
var hasHeader = true;
var rowHeight = contentHeight + (hasHeader ? 50 : 20);
// rowHeight is equal to 90

The example above is shorthand for the code below:

var contentHeight = 40;
var hasHeader = true;
var rowHeight;

if hasHeader {
    rowHeight = contentHeight + 50;

} else {
    rowHeight = contentHeight + 20;
}

// rowHeight is equal to 90

The first example’s use of the ternary conditional operator means that rowHeight can be set to the correct value on a single line of code, which is more concise than the code used in the second example.

The ternary conditional operator provides an efficient shorthand for deciding which of two expressions to consider. Use the ternary conditional operator with care, however. Its conciseness can lead to hard-to-read code if overused. Avoid combining multiple instances of the ternary conditional operator into one compound statement.

Logical Operators

Logical operators modify or combine the Boolean logic values true and false. ObjectTalk supports the three standard logical operators found in C-based languages:

  • Logical NOT (!a)
  • Logical AND (a && b)
  • Logical OR (a || b)

Logical NOT Operator

The logical NOT operator (!a) inverts a Boolean value so that true becomes false, and false becomes true.

The logical NOT operator is a prefix operator, and appears immediately before the value it operates on, without any white space. It can be read as “not a”, as seen in the following example:

var allowedEntry = false;

if !allowedEntry {
    print("ACCESS DENIED");
}

// Prints "ACCESS DENIED"

The phrase if !allowedEntry can be read as “if not allowed entry.” The subsequent line is only executed if “not allowed entry” is true; that is, if allowedEntry is false.

As in this example, careful choice of Boolean constant and variable names can help to keep code readable and concise, while avoiding double negatives or confusing logic statements.

Logical AND Operator

The logical AND operator (a && b) creates logical expressions where both values must be true for the overall expression to also be true.

If either value is false, the overall expression will also be false. In fact, if the first value is false, the second value won’t even be evaluated, because it can’t possibly make the overall expression equate to true. This is known as short-circuit evaluation.

This example considers two Bool values and only allows access if both values are true:

var enteredDoorCode = true;
var passedRetinaScan = false;

if enteredDoorCode && passedRetinaScan {
    print("Welcome!");

} else {
    print("ACCESS DENIED");
}

// Prints "ACCESS DENIED"

Logical OR Operator

The logical OR operator (a || b) is an infix operator made from two adjacent pipe characters. You use it to create logical expressions in which only one of the two values has to be true for the overall expression to be true.

Like the Logical AND operator above, the Logical OR operator uses short circuit evaluation to consider its expressions. If the left side of a Logical OR expression is true, the right side isn’t evaluated, because it can’t change the outcome of the overall expression.

In the example below, the first Bool value (hasDoorKey) is false, but the second value (knowsOverridePassword) is true. Because one value is true, the overall expression also evaluates to true, and access is allowed:

var hasDoorKey = false;
var knowsOverridePassword = true;

if hasDoorKey || knowsOverridePassword {
    print("Welcome!");

} else {
    print("ACCESS DENIED");
}

// Prints "Welcome!"

Combining Logical Operators

You can combine multiple logical operators to create longer compound expressions:

if enteredDoorCode && passedRetinaScan || hasDoorKey || knowsOverridePassword {
    print("Welcome!");

} else {
    print("ACCESS DENIED");
}

// Prints "Welcome!"

This example uses multiple && and || operators to create a longer compound expression. However, the && and || operators still operate on only two values, so this is actually three smaller expressions chained together. The example above is evaluated from left to right but it would probably make sense to use parenthesis for readability (see below).

If we’ve entered the correct door code and passed the retina scan, or if we have a valid door key, or if we know the emergency override password, then allow access.

Based on the values of enteredDoorCode, passedRetinaScan, and hasDoorKey, the first two subexpressions are false. However, the emergency override password is known, so the overall compound expression still evaluates to true.

Explicit Parentheses

It’s sometimes useful to include parentheses when they’re not strictly needed, to make the intention of a complex expression easier to read. In the door access example above, it’s useful to add parentheses around the first part of the compound expression to make its intent explicit:

if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword {
    print("Welcome!");

} else {
    print("ACCESS DENIED");
}

// Prints "Welcome!"

The parentheses make it clear that the first two values are considered as part of a separate possible state in the overall logic. The output of the compound expression doesn’t change, but the overall intention is clearer to the reader. Readability is always preferred over brevity; use parentheses where they help to make your intentions clear.

Operator Overview and Priorities

The table below list all the operators used in ObjectTalk including their priority and member function name.

Operator Description Priority Member Function
[] Index 1 __index__
() Call 1 __call__
. Member 1 __member__
++ Post-increment 1 __inc__
-- Post-decrement 1 __dec__
- Unary minus 2 __neg__
+ Unary plus 2 __plus__
! Logical NOT 2 __not__
~ Bitwise NOT 2 __bnot__
++ Pre-increment 2 __inc__
-- Pre-decrement 2 __dec__
* Multiply 3 __mul__
/ Divide 3 __div__
** Power 3 __power__
% Remainder 3 __mod__
+ Add 4 __add__
- Subtract 4 __sub__
<< Bitwise left shift 5 __lshift__
>> Bitwise right shift 5 __rshift__
< Less than 6 __lt__
<= Less than or equal 6 __le__
> Greater than 6 __gt__
>= Greater than or equal 6 __ge__
in is in 6 __contains__
not in is not in 6
== Equal 7 __eq__
!= Not equal 7 __ne__
& Bitwise AND 8 __band__
^ Bitwise XOR 9 __bxor__
| Bitwise OR 10 __bor__
&& Logical AND 11 __and__
|| Logical OR 12 __or__
?: Ternary conditional 13
= Assign 14 __assign__
*= Multiply and assign 14
/= Divide and assign 14
%= Remainder and assign 14
+= Add and assign 14
-= Subtract and assign 14
<<= Left bit shift and assign 14
>>= Right bit shift and assign 14
&= Bitwise AND and assign 14
|= Bitwise OR and assign 14
^= Bitwise XOR and assign 14

Control Flow

ObjectTalk provides a variety of control flow statements. These include while loops to perform a task multiple times; if statements to execute different branches of code based on certain conditions. ObjectTalk also provides a for-in loop that makes it easy to iterate over arrays, dictionaries, sets, strings, and other sequences.

If

In its simplest form, the if statement has a single condition. It executes a set of statements only if that condition is true.

var temperatureInFahrenheit = 30;

if temperatureInFahrenheit <= 32 {
	print("It's very cold. Consider wearing a scarf.");
}

// Prints "It's very cold. Consider wearing a scarf."

The example above checks whether the temperature is less than or equal to 32 degrees Fahrenheit (the freezing point of water). If it is, a message is printed. Otherwise, no message is printed, and code execution continues after the if statement’s closing brace.

The if statement can provide an alternative set of statements, known as an else clause, for situations when the if condition is false. These statements are indicated by the else keyword.

var temperatureInFahrenheit = 40;

if temperatureInFahrenheit <= 32 {
	print("It's very cold. Consider wearing a scarf.");

} else {
	print("It's not that cold. Wear a t-shirt.");
}

// Prints "It's not that cold. Wear a t-shirt."

One of these two branches is always executed. Because the temperature has increased to 40 degrees Fahrenheit, it’s no longer cold enough to advise wearing a scarf and so the else branch is triggered instead.

You can chain multiple if statements together to consider additional clauses by using the elif statement.

var temperatureInFahrenheit = 90;

if temperatureInFahrenheit <= 32 {
	print("It's very cold. Consider wearing a scarf.");

} elif temperatureInFahrenheit >= 86 {
	print("It's really warm. Don't forget to wear sunscreen.");

} else {
	print("It's not that cold. Wear a t-shirt.");
}

// Prints "It's really warm. Don't forget to wear sunscreen."

Here, an additional elif statement was added to respond to particularly warm temperatures. The final else clause remains, and it prints a response for any temperatures that are neither too warm nor too cold.

The final else clause is optional, however, and can be excluded if the set of conditions doesn’t need to be complete.

var temperatureInFahrenheit = 72;

if temperatureInFahrenheit <= 32 {
	print("It's very cold. Consider wearing a scarf.");

} elif temperatureInFahrenheit >= 86 {
	print("It's really warm. Don't forget to wear sunscreen.");
}

Because the temperature is neither too cold nor too warm to trigger the if or else if conditions, no message is printed.

While Loops

A while loop performs a set of statements until a condition becomes false. These kinds of loops are best used when the number of iterations isn’t known before the first iteration begins. ObjectTalk provides two kinds of while loops:

  • while evaluates its condition at the start of each pass through the loop.
  • do-while evaluates its condition at the end of each pass through the loop.

While

A while loop starts by evaluating a single condition. If the condition is true, a set of statements is repeated until the condition becomes false.

Here’s the general form of a while loop:

while condition {
	statements
}

This example plays a simple game of Snakes and Ladders (also known as Chutes and Ladders):

The rules of the game are as follows:

  • The board has 25 squares, and the aim is to land on or beyond square 25.
  • The player’s starting square is “square zero”, which is just off the bottom-left corner of the board.
  • Each turn, you roll a six-sided dice and move by that number of squares, following the horizontal path indicated by the dotted arrow above.
  • If your turn ends at the bottom of a ladder, you move up that ladder.
  • If your turn ends at the head of a snake, you move down that snake.

The game board is represented by an array of integers. Its size is based on a constant called finalSquare, which is used to initialize the array and also to check for a win condition later in the example. Because the players start off the board, on “square zero”, the board is initialized with 26 zero Int values, not 25.

var finalSquare = 25;

var board = Array();
board.fill(finalSquare + 1, 0);

Some squares are then set to have more specific values for the snakes and ladders. Squares with a ladder base have a positive number to move you up the board, whereas squares with a snake head have a negative number to move you back down the board.

board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02;
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08;

Square 3 contains the bottom of a ladder that moves you up to square 11. To represent this, board[03] is equal to +08, which is equivalent to an integer value of 8 (the difference between 3 and 11). To align the values and statements, the unary plus operator (+i) is explicitly used with the unary minus operator (-i) and numbers lower than 10 are padded with zeros.

var square = 0;
var diceRoll = 0;

while square < finalSquare {
	// roll the dice
	diceRoll += 1;

	if diceRoll == 7 {
		diceRoll = 1;
	}

	// move by the rolled amount
	square += diceRoll;

	// if we're on the board, move up or down for a snake or ladder
	if square < board.count {
		square += board[square];
	}
}

print("Game over!");

The example above uses a very simple approach to dice rolling. Instead of generating a random number, it starts with a diceRoll value of 0. Each time through the while loop, diceRoll is incremented by one and is then checked to see whether it has become too large. Whenever this return value equals 7, the dice roll has become too large and is reset to a value of 1. The result is a sequence of diceRoll values that’s always 1, 2, 3, 4, 5, 6, 1, 2 and so on.

After rolling the dice, the player moves forward by diceRoll squares. It’s possible that the dice roll may have moved the player beyond square 25, in which case the game is over. To cope with this scenario, the code checks that square is less than the board array’s count property. If square is valid, the value stored in board[square] is added to the current square value to move the player up or down any ladders or snakes.

The current while loop execution then ends, and the loop’s condition is checked to see if the loop should be executed again. If the player has moved on or beyond square number 25, the loop’s condition evaluates to false and the game ends.

A while loop is appropriate in this case, because the length of the game isn’t clear at the start of the while loop. Instead, the loop is executed until a particular condition is satisfied.

Do-While

The other variation of the while loop, known as the do-while loop, performs a single pass through the loop block first, before considering the loop’s condition. It then continues to repeat the loop until the condition is false.

Here’s the general form of a do-while loop:

do {
	statements
} while condition;

Here’s the Snakes and Ladders example again, written as a do-while loop rather than a while loop. The values of finalSquare, board, square, and diceRoll are initialized in exactly the same way as with a while loop.

var finalSquare = 25;

var board = Array();
board.fill(finalSquare + 1, 0);

board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02;
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08;

var square = 0;
var diceRoll = 0;

In this version of the game, the first action in the loop is to check for a ladder or a snake. No ladder on the board takes the player straight to square 25, and so it isn’t possible to win the game by moving up a ladder. Therefore, it’s safe to check for a snake or a ladder as the first action in the loop.

At the start of the game, the player is on “square zero”. board[0] always equals 0 and has no effect.

do {
	// move up or down for a snake or ladder
	square += board[square];

	// roll the dice
	diceRoll += 1;

	if diceRoll == 7 {
		diceRoll = 1;
	}

	// move by the rolled amount
	square += diceRoll;
} while square < finalSquare;

print("Game over!");

After the code checks for snakes and ladders, the dice is rolled and the player is moved forward by diceRoll squares. The current loop execution then ends.

The loop’s condition (while square < finalSquare) is the same as before, but this time it’s not evaluated until the end of the first run through the loop. The structure of the do-while loop is better suited to this game than the while loop in the previous example. In the do-while loop above, square += board[square] is always executed immediately after the loop’s while condition confirms that square is still on the board. This behavior removes the need for the array bounds check seen in the while loop version of the game described earlier.

For-In Loops

You use the for-in loop to iterate over a sequence, such as items in an array, dictionary, set or characters in a string.

This example uses a for-in loop to iterate over the items in an array:

var names = ["Anna", "Alex", "Brian", "Jack"];

for name in names {
	print("Hello, ", name, "!");
}

// Hello, Anna!
// Hello, Alex!
// Hello, Brian!
// Hello, Jack!

To use a preset range, you can use the range generator:

for number in range(4) {
	print(number);
}

// prints 0 1 2 3

for number in range(2, 6) {
	print(number);
}

// prints 2 3 4 5 6

for number in range(5, 9, 2) {
	print(number);
}

// prints 5 7 9

for number in range(10, 6, -2) {
	print(number);
}

// prints 10 8 6

Scope

Scope is a concept that refers to where objects including functions and classes can be seen and accessed. In ObjectTalk, variables are declared and stored in a scope. The scope of a variable describes where in a program's text the variable may be used, while the extent or lifetime describes when in a program's execution a variable has a value.

ObjectTalk uses lexical scoping (a detailed explanation can be found on Wikipedia. In lexical scoping, name resolution depends on the location in the source code and the lexical context, which is defined by where the named object is defined.

ObjectTalk implements 5 different scope types:

  • Global Scope - When the ObjectTalk interpreter is started, a default global scope is created. This is the root scope and contains a small set of useful variables (null, true, false), a few high level functions (import, print, and assert) and the list of classes that can be instantiated by the user. For a detailed description see the documentation on the Global class. Global scope members are stored on the heap.
  • Module Scope - An ObjectTalk module is derived from a single source code file. Any object defined at the module's root level is added to the module's scope. The effect this achieves is that those objects have global visibility in that module and can simply be addressed by their name. If a module is imported into another module, the variables of the imported module can be accessed by the importer using the member notation (dot operator). Module scope members are stored on the heap.
  • Class Scope - In ObjectTalk, classes represent a separate scope. Any object (variables, functions, subclasses) defined in a class become part of the class scope. These object can be addressed inside the class by their name and using the member notation (dot operator) outside of the class. Class scope members are stored on the heap.
  • Function Scope - In ObjectTalk, functions represent a separate scope. Unlike the previous 3 types, function scope objects live on the stack as their lifetime tends to be short. The function scope includes the calling parameters.
  • Block Scope - In ObjectTalk, every block (code contained in curly brackets({})). has it's own scope. Variables contained in blocks are stored on the stack as the are also very short lived.

ObjectTalk does have a mechanism to capture variables from a parent scope using a closure mechanism. The special mechanism which is often required in lambda function or just because functions are first class citizens in ObjectTalk, is described in in the closure section.

Modules

A module is a single unit of code that’s built and shipped as a single unit and that can be imported by another module with ObjectTalk's import function.

A source file is a single ObjectTalk module (in effect, a single file within an app). Although it’s common to define individual types in separate source files, a single source file can contain definitions for multiple classes, functions, and so on.

When the ObjectTalk interpreter is started, it will load the specified script (source code) as the first module. Additional modules can be loaded with the import function. Here is an example based on two files; one containing a class definition and the other containing the main program.

// Module "counter.ot"

class Counter : Object {
	function__init__(this) {
		this.count = 0;
	}

	function increment(this) {
		this.count += 1;
	}
	function increment(this, amount) {
		this.count += amount;
	}
	function reset(this) {
		this.count = 0;
	}
}

// Module "app.ot"

var counter = import("counter");

var tracker = counter.Counter(); // the initial counter value is 0
assert(tracker.count == 0);

tracker.increment();             // the counter's value is now 1
assert(tracker.count == 1);

tracker.increment(5);            // the counter's value is now 6
assert(tracker.count == 6);

tracker.reset();                 // the counter's value is now 0
assert(tracker.count == 0);

Classes

Classes are general-purpose, flexible constructs that become the building blocks of your program’s code. You define properties and member functions to add functionality to your classes using the same syntax you use to define variables and functions.

An instance of a class is traditionally known as an object. However, in this ObjectTalk documentation the terms object and instance are used interchangeably.

Definition Syntax

You introduce classes with the class keyword:

class Resolution : Object {
	var width = 0;
	var height = 0;
}

The example above defines a new class called Resolution (that is derived from the Object class), to describe a pixel-based display resolution. This class has two class variables called width and height. Class variables are variables that are stored as part of the class. All instances of Resolution share these variables. To have unique variables per instances we need to assign values to the instance member.

class Resolution : Object {
	// instance member initialized in constructor
	function__init__(this, width, height) {
		this.width = 0;
		this.height = 0;
	}
}

class VideoMode : Object {
	var classVariable = "mode class";

	function__init__(this) {
		this.resolution = Resolution(1920, 1080);
		this.interlaced = false;
		this.frameRate = 0.0;
		this.name = null;
	}
}

var mode = VideoMode();
mode.name = "Test Mode"; // assign value to instance variable

The example above also defines a new class called VideoMode, to describe a specific video mode for video display. This class has four instance variables. The first, resolution, is initialized with a new Resolution instance, which infers a property type of Resolution. For the other three properties, new VideoMode instances will be initialized with an interlaced setting of false (meaning “noninterlaced video”), a playback frame rate of 0.0, and an optional String value called name.

Class Instances

The Resolution and the VideoMode classes definition only describe what a Resolution or VideoMode will look like. They themselves don’t describe a specific resolution or video mode. To do that, you need to create an instance of the structure or class.

var someResolution = Resolution(1920, 1080);
var someVideoMode = VideoMode();

The simplest form of initializer syntax uses the type name of the class followed by empty parentheses, such as VideoMode(). This creates a new instance of the class. In some cases (like the Resolution class above), the class constructor (__init__) can take additional parameters to initialize a class instance.

Accessing Members

You can access the members of an instance using dot syntax. In dot syntax, you write the property name immediately after the instance name, separated by a dot (.):

print("The width of someResolution is ", someResolution.width);
// Prints "The width of someResolution is 1920"

In this example, someResolution.width refers to the width property of someResolution, and returns its default initial value of 0.

You can drill down into sub-properties, such as the width property in the resolution property of a VideoMode:

print("The width of someVideoMode is ", someVideoMode.resolution.width");
// Prints "The width of someVideoMode is 1920"

You can also use dot syntax to assign a new value to a variable property:

someVideoMode.resolution.width = 1280;
print("The width of someVideoMode is ", someVideoMode.resolution.width");
// Prints "The width of someVideoMode is 1280"

The dot notations can be used to access both instance and class member variables. When reading a member, the runtime will first look in the instance and if it can't be found the runtime will search the class and all it's parent classes. Here is an example:

class Root : Object {
	var value0 = 0;
	var value1 = 1;
}

class Child : Root {
	var value0 = false;
}

class GrandChild : Child {
	var value2 = 2;
	var value3 = 3;
}

var grandChild = GrandChild();
grandChild.value3 = true;

assert(grandChild.value0 == false);
assert(grandChild.value1 == 1);
assert(grandChild.value2 == 2);
assert(grandChild.value3 == true);

When setting a member, the dot notation can be used to set instance and class members like:

grandChild.age = 5; // setting an instance member
GrandChild.value3 = null; // setting a class member;

Member Functions

Member functions are associated with classes and encapsulate specific tasks and functionality for working with an instance of a given class.

You write an member functions within the opening and closing braces of the class they belongs to. A member function can be called only on a specific instance of the class it belongs to.

Here’s an example that defines a simple Counter class, which can be used to count the number of times an action occurs:

class Counter : Object {
	function__init__(this) {
		this.count = 0;
	}

	function increment(this) {
		this.count += 1;
	}

	function increment(this, amount) {
		this.count += amount;
	}

	function reset(this) {
		this.count = 0;
	}
}

The Counter class defines three instance methods:

  • increment(this) - increments the counter by 1.
  • increment(this, amount) - increments the counter by a specified amount.
  • reset(this) - resets the counter to zero.

You call instance methods with the same dot syntax as properties:

var counter = Counter(); // the initial counter value is 0
counter.increment();     // the counter's value is now 1
counter.increment(5);    // the counter's value is now 6
counter.reset();         // the counter's value is now 0

The this Property

Notice in the examples above how "this" is used to distinguish the name property from the name argument to the initializer. In some computer language, the "this" or "self" argument is automatically added to a member function. In ObjectTalk this is not the case and you have to name it yourself as the first parameter of a member function. The benefit is that you can call it whatever you want and it's not some hidden feature of the language. The disadvantage is that you have to type it for every member function. The compiler automatically translates a bound function (a function in the context of an object) to a new function call with an additional parameter.

counter.increment(2); // becomes:
Counter.increment(counter, 2);

Constructor

Initialization is the process of preparing an instance of a class for use and it's performed by the class' constructor. This process involves setting an initial value for member variables on that instance and performing any other setup or initialization that’s required before the new instance is ready for use.

You implement this initialization process by defining constructors, which are like special member functions that can be called to create a new instance of a particular type. Their primary role is to ensure that new instances of a type are correctly initialized before they’re used for the first time.

The name of the constructor is always __init__ as you can see in the examples above. Here's another example:

class Fahrenheit : Object {
	function __init__(this) {
		this.temperature = 32;
	}
}

var f = Fahrenheit();
print("The default temperature is ", f.temperature, "° Fahrenheit")
// Prints "The default temperature is 32° Fahrenheit"

In case the class is derived from a class that also has a constructor, the superclass constructor must also be called like this:

class Temperature : Object {
	function __init__(this) {
		this.measurement = "temperature";
	}
}

class Fahrenheit : Temperature {
	function __init__(this) {
		super.__init__(this);
		this.temperature = 32;
	}
}

Operators

In ObjectTalk, everything is an Object derived from a Class. The number 1 is an object derived from the Integer class and so are 3.14 (derived from the Real class) and "test" (derived from the string class). To take this a step further, operators like +, -, *, /, [] and () are actually member functions of a class.

To ensure this doesn't lead to silly syntax, the ObjectTalk compiler translates more traditional expressions into a set of member function calls. So for instances, the follow example shows this translation:

1 + (2 * 3)

2.__mul__(3).__add__(1)

As a user of ObjectTalk you only have to learn the top syntax. If you want to understand the compiler and VM, you have to understand both.

Given that operators become member function calls, custom Classes can implement operators so it is very simple in ObjectTalk to implement a Vector class that implements a dot product operator.

You can find a list of all operator function member name in the Operators section.

Subclassing

You can subclass any class as ObjectTalk supports the concept of single inheritance (meaning a class can derive from a single other class). In fact all classes in ObjectTalk have a parent class with the exception of the Object class which is the root class for all others.

class A : Object {
	function someMethod(this) {
		print("A.someMethod()");
	}
}

class B : A {
	function someMethod(this) {
		print("B.someMethod()");
	}
}

It’s valid for a subclass member to call a superclass member using the super special keyword.

class A : Object {
	function someMethod(this, value) {
		print("A.someMethod(", value, ")");
	}
}

class B : A {
	function someMethod(this, value) {
		super.someMethod(this, value);
		print("B.someMethod(", value, ")");
	}
}

var b = B();
b.someMethod(2);

Please note that the super mechanism can only be used in a class definition and it is basically a shortcut to the superclass' specified member function. As such, the resulting reference is unbound and you have to pass the target object as the first parameter in the member function call.

In the above example b.someMethod(2) is translated into a bound member function call and someMethod is called with b and 2 as parameters. super.someMethod is an unbound member function call and you will have to pass the object.

Functions

Functions are self-contained chunks of code that perform a specific task. ObjectTalk has three function "types". First there is the traditional function as you would define it in other languages. Secondly there is the "lambda" version of a function which makes a function look more like any other primitive. In fact, the only difference between these first two types is syntax. The third type is the member function which is a function attached to a class and performs a task on an instance of that class. This is what they looks like:

// traditional function definition
function callMeTraditional(parameter) {
	return parameter + 1;
}

// "lambda" style function
var callMeLambda = function(parameter) {
	return parameter + 1;
}

// member function
class newestObjectClass : Object {
	var increment = 1;

	function callMeMemberFunction(this, parameter) {
		return parameter + this.increment;
	}
}

Traditional and lambda functions will be described in this section. Member Functions are covered in the Classes section.

When you define a function, you can optionally define one or more named, values that the function takes as input, known as parameters. Function parameters and return values are extremely flexible in ObjectTalk. You can define anything from a simple utility function with a single parameter to a complex function with expressive parameters.

Functions Without Parameters

Functions aren’t required to define input parameters. Here’s a function with no input parameters, which always returns the same String message whenever it’s called:

function sayHelloWorld() {
	return "hello, world";
}

print(sayHelloWorld());
// Prints "hello, world"

The function definition still needs parentheses after the function’s name, even though it doesn’t take any parameters. The function name is also followed by an empty pair of parentheses when the function is called.

Functions With Multiple Parameters

Functions can have multiple input parameters, which are written within he function’s parentheses, separated by commas.

This function takes a person’s name and whether they have already been greeted as input, and returns an appropriate greeting for that person:

function greet(person, alreadyGreeted) {
	if (alreadyGreeted) {
		return greetAgain(person);

	} else {
		return greet(person);
	}
}

print(greet("Tim", true));
// Prints "Hello again, Tim!"

Functions Without Return Values

Functions aren’t required to return a value. Here’s a version of the greet function, which prints its own String value rather than returning it:

function greet(person) {
	print("Hello, ", person, "!");
}

greet("Dave");
// Prints "Hello, Dave!"

Strictly speaking, this version of the greet() function does still return a value, even though no return value is defined. Functions without a defined return will automatically return null.

Functions as Return Value

You can use a function as the return value of another function. The next example defines two simple functions called stepForward() and stepBackward(). The stepForward() function returns a value one more than its input value, and the stepBackward() function returns a value one less than its input value.

function stepForward(input) {
	return input + 1;
}

function stepBackward(input) {
	return input - 1;
}

Here’s a function called chooseStepFunction() which returns the stepForward() function or the stepBackward() function based on a Boolean arameter called backward:

function chooseStepFunction(backward) {
	return backward ? stepBackward : stepForward;
}

You can now use chooseStepFunction() to obtain a function that will step in one direction or the other:

var currentValue = 3;
var moveNearerToZero = chooseStepFunction(currentValue > 0);
// moveNearerToZero now refers to the stepBackward() function

The example above determines whether a positive or negative step is needed to move a variable called currentValue progressively closer to zero. currentValue has an initial value of 3, which means that currentValue > 0 returns true, causing chooseStepFunction(backward:) to return the stepBackward() function. A reference to the returned function is stored in a constant called moveNearerToZero.

Now that moveNearerToZero refers to the correct function, it can be used to count to zero:

print("Counting to zero:");

// Counting to zero:
while (currentValue != 0) {
	print(currentValue, "... ");
	currentValue = moveNearerToZero(currentValue);
}

print("zero!");
// 3...
// 2...
// 1...
// zero!

Nested Functions

All of the functions you have encountered so far in this chapter have been examples of global functions, which are defined at a global or module scope. You can also define functions inside the bodies of other functions, known as nested functions.

Nested functions are hidden from the outside world by default, but can still be called and used by their enclosing function. An enclosing function can also return one of its nested functions to allow the nested function to be used in another scope.

You can rewrite the chooseStepFunction() example above to use and return nested functions:

function chooseStepFunction(backward) {
	function stepForward(input) { return input + 1 }
	function stepBackward(input) { return input - 1 }
	return backward ? stepBackward : stepForward;
}

var currentValue = -4;
var moveNearerToZero = chooseStepFunction(currentValue > 0);

// moveNearerToZero now refers to the nested stepForward() function
while (currentValue != 0) {
	print(currentValue, "...");
	currentValue = moveNearerToZero(currentValue);
}

print("zero!");

// -4...
// -3...
// -2...
// -1...
// zero!