Introduction to Serpentine

This document aims to cover the basic syntax and semantics of the Serpentine language. Although the syntax is compatible with that of the Python language, its semantics differ in a number of ways, mostly due to the constraints of the Dalvik virtual machine. This means that some programs will behave differently to the way they would run in a Python interpreter.

We do not cover package building in any detail in this document. See the Getting Started document for a brief guide to building a package.

Variables and Types

As in Python, variables do not need to be declared. Values are simply assigned to names as they are needed:

a = 123
b = float(4.0)
c = long(4294967296)
d = -1.1e64
text = "Hello"
paint = Paint()

Note the use of float and long built-in functions. The default type for integers is the 32-bit int type, and the default type for floating point numbers is the 64-bit double type. The int, float, long and double built-in functions can be used to cast values from one type to another. However, when specifying constants as in the above code, the functions are simply used to indicate that a constant should be encoded using a specific type - no method calls are generated in this case.

Unlike Python, once a variable has been defined, its type is fixed. You cannot assign a value of a different type to it unless the value can be implicitly cast to the variable's type:

a = 123
b = float(4.0)
a = b           # a = 4 (integer)
d = long(4294967296)
e = 1
f = d + e       # f = 4294967297 (long)

Implicit casting is only performed for primitive numeric types. This is useful when passing values of a certain type to standard Java or Android APIs that expect different, but compatible types.

Instances of classes can be created in the usual way. The action of creating an instance associates its type with the variable it is assigned to, making it possible to call instance methods:

view = TextView(self)
view.setText("Hello world!")

Python's built-in collection types - lists, dictionaries, tuples and sets - are represented in some form in Serpentine, but have different behaviours since they are based on the more restrictive Java types: ArrayList, HashMap and HashSet. For convenience, the Python notation used for lists, dictionaries, tuples and sets is supported by the compiler, but simply maps to these types.

l = ["Hello", "World"]
t = ("A", "B", "C")
d = {"ABC": 123, "DEF", 456}
s = {"alpha", "beta", "gamma"}

Arrays are also supported in the language and can be created using the array built-in function. This takes two forms: a two argument version that allows an empty array to be created, and a one argument version that converts an existing collection to an array. The following two examples demonstrate these two forms:

# Create an empty array with space for 4 integers.
a = array(int, 4)

# Create a string array from an existing string list.
a = array(["The", "quick", "brown", "fox"])

Control Flow

The basic control flow structures are available. These include if statements, while loops and for loops. There are some restrictions on what can be done in these structures that may be surprising to Python programmers.

Here is an example if...elif...else structure that behaves the same way as it would when run in a Python interpreter:

if i < 1:
    s = "Less than 1\n"
elif i == 1:
    s = "Equals 1\n"
else:
    s = "Greater than 1\n"

One point worth making is that the string variable, s, is not defined before the structure, yet the intention in the above code is to make the variable available for use in later code in the same method. This can only be done if the variable is defined in all of the possible code paths through the structure. If not, the variable will be defined locally to the structure and cannot be accessed afterwards.

The simplest type of loop is the while loop:

s = ""
i = 0
while i < 10:
    s += str(i) + " "
    i = i + 1

expected = "0 1 2 3 4 5 6 7 8 9 "

The else clause from Python is also supported for while loops. The else clause is only executed if the loop is exited via its continuation condition. If the loop is exited via the break keyword, the else clause is skipped:

s = ""
i = 0
while i < 10:
    s += str(i) + " "
    if i == 5:
        break
    i = i + 1
else:
    s += str(i)

expected = "0 1 2 3 4 5 "

The variables used for the loop's condition must already be defined before they are used in the loop. The limitations on defining variables inside the loop that we had with the if statement also apply for while loops. New variables defined inside the loop are only accessible outside it if they are also defined in an else clause. The only exception with this is if the loop is unconditionally executed:

while True:
    s = get_input()
    if s != "":
        break

The other type of loop is the for loop. This is used to iterate over the contents of a collection:

s = ""
l = [0, 1, 2, 3, 4, 5]
for i in l:
    s += str(i) + " "

As well as lists, it is also possible to iterate over arrays, sets and tuples with a for loop. As with while loops, break statements can be used to exit the loop, continue statements cause execution to return to the start of the loop for the next iteration, and else clauses are executed if no break keyword is encountered.

Classes, Methods and Interfaces

Classes are defined as in Python, but with some limitations and differences in how they are presented to the virtual machine. Unlike in Python, classes can only be derived from at most one base class:

class HelloActivity(Activity):

    def __init__(self):
        Activity.__init__(self)

Methods are defined in the same way as in Python. Where a method is a reimplementation of a method in a base class or interface, it can be defined without annotating it with a decorator:

    def onCreate(self, bundle):
    
        Activity.onCreate(self, bundle)
        
        view = TextView(self)
        view.setText("Hello world!")
        self.setContentView(view)

Classes can be defined without a base class, in which case the result is an interface:

class ValueInterface:

    @args(int, [int])
    def getValue(self, x):
        pass

Because the method is only defined here, a decorator is needed in order to define the return type and the method's argument types. The argument types are always defined in a list, even if there is only one of them. The self argument is never included in the list.

Although classes can only inherit from one base class, they can implement multiple interfaces. These are declared with the __interfaces__ class attribute, which is a list of interface classes:

class InterfacesActivity(TestActivity):

    __interfaces__ = [ValueInterface]

    # ...

    def getValue(self, x):
        return x * 2

This indicates that the class provides implementation of the methods defined by the interfaces in the list. In the above example, the InterfacesActivity class is declaring that it implements the methods defined by the ValueInterface interface class defined earlier.

Declaring that a class implements an interface enables instances of that class to guarantee that they provide implementations of certain methods without tying the implementation to that class or its subclasses. Another unrelated class can define the same interface and be used interchangeably with the class defined above in cases where it is only important that an object provides a getValue method with that particular signature.

Classes can contain static methods that are invoked directly on a class instead of being invoked on an instance of it. These are defined in the same way as regular methods, except that they are annotated with the @static decorator as in the following example:

class Other(Object):

    @static
    @args(String, [int])
    def fn(i):
        return str(i)

Note that we retain the convention used in Python for static methods, omitting the unnecessary self parameter that is defined in regular methods.

Exceptions

Exceptions are generally used in the same way as in Python, with some limitations. The biggest omission is the lack of support for the finally keyword.