Introduction to Mars

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.

Note

If I’m describing command line input, I’ll use $ to denote the Unix prompt (such as Bash), and ?> for the Mars interactive prompt.

What is Mars?

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.

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.

Starting off

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.

Interactive mode

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:

$ mars

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 Num 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. These can be used to perform simple arithmetic:

?> add(4.2, mul(9, 2))
22.2

Mars also has the usual binary operators (with the normal precedence and associativity) as a short-hand notation for the above functions, so you can treat the Mars interpreter like a calculator:

?> 4.2 + 9 * 2
22.2

Hello World program

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() :: io Num:
    print_string("Hello, world!\n")
    return 0

Note

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 a Num, 0. You may also note the special keyword io, which says that main is allowed to perform side-effects such as printing.

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.

Note

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.

Basic syntax

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 compare_to_seven(x :: Num) :: Num:
    if x == 7:
        return 0    # x is 7
    elif x > 7:
        return 1    # x is greater than 7
    else:
        return -1   # x is less than 7

Note that you may optionally specify elif (“else if”) and else clauses. If there is no else clause and none of the other conditions match, the if statement does nothing.

Note

Unlike main, compare_to_seven does not feature the io keyword, because it doesn’t perform any side-effects (it is a “pure” function – it just returns a value). You should not use the io annotation unless you need to perform some I/O effect.

Variables and statements

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 :: Num) :: Num:
    var fac :: Num
    fac = 1
    while n > 1:
        fac = fac * n
        n = 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 :: Num) :: Num:
    if n > 1:
        return n * factorial(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() :: io Num:
    print_value(factorial(5))
    print_string("\n")
    return 0

Note

print_value can print out any value in its canonical form. It does this by calling print_string(show(value)). show converts the value into a string, and print_string prints it out.

Switch statements

Mars features a switch statement, similar to the one found in C-like languages. It’s like an if statement, but lets you make decisions across a range of values. For example:

def foo(x :: Num) :: io Num:
    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 a Num. 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.

Note

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.

Arrays

Mars allows the definition of array values using the array literal notation:

var a :: Array(Num)
a = [1, 1.5, 2, 2.5, 3]

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 numbers, representing the characters’ ASCII values:

?> "hello"
[104, 101, 108, 108, 111]

Whenever you deal with strings, you should be sure that the array values are all whole numbers within the range 0 – 255 inclusive. Values above 127 should be treated as UTF-8 code units. Most string-handling functions will raise an error if either of these conditions are not met.

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(Num)) :: Array(Num):
    var i :: Num
    i = 0
    while i < array_length(a):
        a = array_replace(a, i, array_ref(a, i) + 1)
        i = i + 1
    return a
?> array_increment([1,2,3])
[2, 3, 4]

Note

Because array_replace is non-destructive, it can be inefficient to run it in a loop. The Mars compiler has sophisticated optimizations to avoid unnecessary copying, but they aren’t effective in all cases. See Automatic destructive update for a detailed discussion on this optimization process.

Types

You can find out the type of any variable, function or expression in the interactive mode using the ”:t” command, like this:

?> :t 4
4 :: Num
?> :t [1, 2, 3]
[1, 2, 3] :: Array(Num)
?> x = 4
?> :t x
x :: Num
?> :t add
add :: (Num, Num) -> Num

The last example shows the type of a function, in functional notation (which is covered in the next section). This means the add function takes two numbers, and returns a number.

Functional notation

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 :: Num) :: Num:
    return n + 1

Can be written more succinctly as:

def increment(n :: Num) :: Num = 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 :: Num = 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() :: Num = 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

Functions in Mars are first-class. That means you can treat any function as if it were a value, as we saw with the function ten above. For example, we can define increment in this (silly) way:

def increment(n :: Num) :: Num:
    var f :: (Num, Num) -> Num
    f = add
    return f(n, 1)

Note that because we are not calling it directly, we must use the full name add rather than the short-hand + operator.

In the above code, we assign the function add to a local variable f. Later, we call f, which calls whatever function we put in it earlier. Note the type of f, (Num, Num) -> Num, which means “a function expecting two numbers and returning one numbers.” Using this technique, functions can be passed as arguments, returned as results, and stored in data structures.

Mars also supports explicit currying – you can pass fewer arguments to a function than it expects, and Mars will create a new function that expects the remaining arguments. For example:

def increment(n :: Num) :: Num:
    var f :: Num -> Num
    f = add(n, ...)
    return f(1)

This version of increment passes just one argument to add (n); the ... signifies that the remaining argument will come later. Then, when we call f, we need supply only the one remaining argument, 1. You may curry as many or as few arguments as you like at a time, and even curry an already-curried function. This leads to the most elegant solution for increment:

def increment :: Num -> Num = add(1, ...)

Here, increment is a computable global constant, the value of which is a function. The body curries 1 into add and returns the curried function. Now, any call to increment will complete the call to add and be added to 1.

IO annotation

As a pure functional language, Mars normally doesn’t allow functions to perform side-effects. Mars distinguishes between two types of side-effects: mutation (such as updating objects and global variables) and input/output (such as reading and writing from files. We have covered the basics of mutation in the previous section. Here, we focus on input/output effects.

The most obvious input/output effect is printing text to the console. A function like print_string is clearly impure: when you call it, you are not interested in its result (it always returns 0), but rather the effect that it has on the state of the computer (displaying text to the screen). More serious side-effects could include reading and writing files or databases, manipulating graphics, and communicating over the network (although Mars doesn’t directly support such features). This is contrast with a “pure” function like factorial, which doesn’t do anything other than returning a result.

There would be no point to programming if you couldn’t do effects at some point or another, so rather than banning them altogether, we try to avoid accidental side-effects by forcing you to declare your intention to use them. That is why all functions that do side-effects must have the io in their header. This should help you structure your code accordingly: try to avoid using io wherever possible, and you will find it simplifies your code. For example, don’t have functions read input from the user; have them take the input as arguments and write a wrapping io function that reads the input.

Note

There are times when you want to quickly print out some value while you are debugging your code, and it would be annoying if you had to put io on all of your code just to do that. That’s why we provide the trace function in the debug module – you can use it to print text to standard error without being in an io context. Just try to use it for debugging and not as part of your actual program.

There is a subtle interaction between io and the functional style described above: you cannot store an io function in a normal function variable. If you could, you could pass that variable as an argument to a non-io function and then it could perform illegal side-effects! Mars has a separate type for io functions, which has the form a -> io b (meaning, “a function that takes arguments a, does some input/output effects, and returns type b”). This is completely incompatible with the normal a -> b function type: you can either deal with io functions or regular functions. For example, the following code prints “x” to the screen, by passing a curried version of put_char to the do_effect function:

def print_x() :: io Num:
    var f :: Num -> io Num
    f = put_char('x', ...)
    return do_effect(f)

def do_effect(f :: () -> io Num) :: io Num:
    return f()

Note

Computable global constants (procedures without arguments) may not be declared io. 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.

This is okay:

def x() :: io Num:

This is illegal:

def x :: io Num:        # ERROR: Constant declared as io.

User-defined types

Note

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 numbers:

type NumList:
    Cons(head :: Num, tail :: NumList)
    Nil

This type has two alternatives. A NumList may be Nil (representing the empty list), or Cons of a Num and a NumList. 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 a NumList:

def intlist_length(l :: NumList) :: Num:
    switch l:
        case Nil:
            return 0
        case Cons(_, xs):
            return intlist_length(xs) + 1
?> intlist_length(Cons(4, Cons(5, Cons(6, Nil))))
3

Note

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 a Num, you are required to have a default case, as you can’t possibly have a case for every number.

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.

Compiling a Mars program into an executable

You can compile a Mars program into a native executable using marsc. See its man page for details.