Welcome to Mars. This document is an informal overview of the language, designed as a tutorial for programmers already familiar with other languages. Please note that there is also a formal specification, which is intended as a reference, not an introduction.
Please be aware that Mars is a toy language, for academic research. It doesn’t have many of the features you might expect from a proper programming language – it’s got just enough to prove my points. You’re probably using this just to see what it’s like. I don’t expect you to write real programs in it.
I’m going to describe the language (in this document) very informally, and admit when things are not ideal.
If I’m describing command line input, I’ll use $ to denote the Unix prompt (such as Bash), and ?> for the Mars interactive prompt.
Mars is a very simple imperative programming language with a catch: all of the functions and expressions are pure. That means when you call a function, it is guaranteed to have no side-effects, like mutating its arguments, much like Haskell or Erlang. But, unlike those other pure languages, Mars gives you all the nice features of imperative programming, like conditional statements and while loops.
(A minor caveat to the side-effects point: Mars does allow impure input and output, for simplicity, and also provides optional array and structure mutation primitives, should you really need them).
Mars also has some other nice features borrowed from functional programming: a strong static type system, algebraic data types, pattern-matching switch statements, and higher-order functions.
Firstly, you’ll want to grab the Mars compiler and build it, using Mercury (the language it is written in). This is described in the setup document.
The program mars is executed from the command line, and is primarily used to execute Mars programs. It can also act as an interactive interpreter, like the Python one. To start off, we’ll use the interactive mode. To launch this, simply run the command:
When called with no arguments, mars defaults to interactive mode, and automatically loads the prelude (named after the Haskell prelude), which is the “standard library” of Mars.
For example, type into the Mars prompt:
?> print_string("Hello, world!\n")
This will print out the familiar text "Hello, world!". It also prints out 0, which is the return value of the function. (All functions in Mars must return a value; there is no “Void” type. Functions that don’t need to return a value, like print_string, just return the Int value 0).
You can see the other functions available (in the prelude) by typing :b into the prompt. You might notice some arithmetic functions, such as add, sub and mul. In keeping very minimal, Mars has no nice binary operators, like + or *, just plain functions. Nevertheless, you can still do arithmetic, and treat the Mars interpreter like a calculator:
?> add(4, mul(9, 2)) 22
Now let’s write a real program. Mars programs mostly consist of function definitions. The syntax is a lot like Python, except you need type declarations. Here is the complete Hello World program:
#!/usr/bin/env mars import prelude def main() :: Int: print_string("Hello, world!\n") return 0
Mars programs must import the prelude in order to use its functions. It is legal not to import the prelude, but you will have access to only a very limited number of functions and types (the “built-ins”). The prelude only appears by default if you start mars with no arguments, as a courtesy.
Here we define the main function, with no arguments. As stated above, functions need to return a value. main just returns an Int, 0.
Save the above program in a file, hello.mar, and run mars on it:
$ mars hello.mar
That’s the standard way to run Mars programs.
In Unix, you can also run Mars programs like any other Unix scripts, if you remember to place the line #!/usr/bin/env mars at the top, and chmod +x the file (make it executable). Consult a Unix manual for help on doing this.
You can also experiment with your own programs in interactive mode, much like we did with the prelude. Just run mars with the -i option:
$ mars -i hello.mar ?> main() Hello, world! 0
Now that that’s all out the way, I can show you some of the features of Mars.
At first glance, Mars is a fairly typical statically-typed language. As stated above, it has syntax much like Python. This means that unlike other languages, which use curly braces or some other symbols to delimit blocks, Mars just uses indentation. I agree with the designers of Python that this makes the code much neater, and enforces good indentation. For example, here is how you would write an if statement:
import prelude def greater_than_seven(x :: Int) :: Int: if gt(x, 7): print_string("x > 7\n") else: print_string("x <= 7\n") return 0
Also, Mars currently has no elif (or “else if”) statement.
Mars will automatically infer the type of any variable assigned in the body of a function. Variables may also be explicitly declared, using the var keyword. All declarations must come at the top of the function.
You can use a while loop just as you might expect. Assignment statements are also quite straightforward, using the = operator, much to the chagrin of my supervisor.
Here is an example using local variables, while loops, and assignment statements to compute a factorial. Note that you can assign to argument variables (which will not modify the caller’s copy; arguments are passed by value).
import prelude # Compute the factorial of n. def factorial(n :: Int) :: Int: var fac :: Int fac = 1 while gt(n, 1): fac = mul(fac, n) n = sub(n, 1) return fac
Readers are reminded that the easiest way to test functions is to run mars in interactive mode, like this:
$ mars -i factorial.mar ?> factorial(5) 120
Of course, you can also write recursive functions, such as this much more natural definition of factorial:
import prelude # Compute the factorial of n. def factorial(n :: Int) :: Int: if gt(n, 1): return mul(n, factorial(sub(n, 1))) else: return 1
If you want to write a program around the function, you can wrap it in a main function like this:
def main() :: Int: print_value(factorial(5)) print_string("\n") return 0
def foo(x :: Int) :: Int: switch x: case 0: print_string("x is zero.\n") case 4: print_string("x is four.\n") case _: print_string("I'm not sure what x is.\n") return 0
Only case statements are allowed inside switch statements. The switch statement picks the first matching case statement and executes it. Note the final case _ – this is the default case, and it is mandatory for switching over an Int. If you don’t want to write a default case, either put a call to error here, if it should never occur, or a pass statement, if you want to do nothing in that case.
Unlike C and many C-derived languages, there is no break statement for finishing a case. Mars simply doesn’t allow fall-through.
The switch statement is actually a lot more powerful than this, as we’ll see later.
Mars allows the definition of array values using the array literal notation:
var a :: Array(Int) a = [1, 2, 3, 4]
This creates arrays of fixed length. Note that the Array data type must be given an argument, which specifies the type of the array elements. All elements of an array must have the same type.
Strings in Mars are actually just arrays of integers, representing the characters’ ASCII values:
?> "hello" [104, 101, 108, 108, 111]
Arrays of arbitrary size may also be created with the array function. There are several primitive functions which operate on arrays: array_ref, array_replace, array_concat and array_length. Because Mars is pure, there is no array assignment primitive — array_replace is used to make a copy of an array with an updated cell. For example, this function adds 1 to each element of an array:
def array_increment(a :: Array(Int)) :: Array(Int): var i :: Int i = 0 while lt(i, array_length(a)): a = array_replace(a, i, add(array_ref(a, i), 1)) i = add(i, 1) return a
?> array_increment([1,2,3]) [2, 3, 4]
We hope to add an analysis which makes array_replace efficient in certain cases, but for now, it makes a copy of the whole array every time. If you really need to mutate an array, there is an unofficial built-in function called __impure_array_set which destructively modifies an array. If you find yourself using these a lot, you can import a module impure and then refer to this function as array_set.
If a function only has a single return statement in it, it can be summarized using the = symbol in the header. For example, the following function:
def increment(n :: Int) :: Int: return add(n, 1)
Can be written more succinctly as:
def increment(n :: Int) :: Int = add(n, 1)
A function may be written without any arguments or argument parentheses. This isn’t actually a function at all — it is called a “computable global constant”, as once evaluated, it is just a value which doesn’t need to be called. However, its body may contain a complex computation, not just a simple value.
def ten :: Int = add(4, 6)
?> ten 10
Note that this is quite different from a function with zero arguments, like our main functions above. You could alternatively define ten as a “nullary function” like this:
def ten() :: Int = add(4, 6)
?> ten <function ten>
Note that the value “ten”, in this case, is an unevaluated function. It requires empty parentheses to evaluate it properly:
?> ten() 10
Computable global constants may not exhibit side-effects, like printing. If a procedure performs input or output, like main, it must be a nullary function instead, with empty parentheses. This allows the caller to explicitly specify when to execute the procedure.
Do not import the prelude module for the examples in this section, or the names will conflict with names already defined there.
Mars programs may define their own algebraic data types (or discriminated unions; data types with several alternatives). These data types may be recursive. For example, one may define a linked-list of integers:
type IntList: Cons(head :: Int, tail :: IntList) Nil
This type has two alternatives. An IntList may be Nil (representing the empty list), or Cons of an Int and an IntList. For example, the value Cons(4, Nil) represents a list with one element, while Cons(4, Cons(5, Cons(6, Nil))) represents a list with three.
The switch statement in Mars can be used for pattern matching over user-defined types. The case statement may bind variables given as arguments to a type’s alternative. This example finds the length of an IntList:
def intlist_length(l :: IntList) :: Int: switch l: case Nil: return 0 case Cons(_, xs): return add(intlist_length(xs), 1)
?> intlist_length(Cons(4, Cons(5, Cons(6, Nil)))) 3
The default case (case _) is not required if you explicitly match all possible alternatives of the data type, as in the example above. It is preferred that you do not have a default case, as the compiler will give you an error if you missed any cases. However, for switching over an Int, you are required to have a default case, as you can’t possibly have a case for every integer.
User-defined types may have type parameters, so they can store any different type (much like arrays).
type List(a): Cons(head :: a, tail :: List(a)) Nil
This definition is found in the prelude; it defines a linked-list type which can hold elements of any type.