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.
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"])
Higher dimensional arrays can also be created. Here's an example that creates a two dimensional array and populates it with values:
b = array(int, 3, 4)
for i in range(3):
for j in range(4):
b[i][j] = i + j
It might be useful to allow arbitrarily dimensioned arrays to be created at
run-time, but this is not yet possible using the array
built-in function.
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 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 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.