Term-level values are units of data which may be stored in variables and data structures, and passed as arguments. Each value has a corresponding type, with each built-in type having a special set of values, and user-defined types having a set of rules that define what values inhabit each type.
This section describes the values that inhabit each type in Mars. It also describes, for each type family, how the values may be constructed, and how values are to be displayed (which affects both their appearance when printed by the interactive prompt, as well as the output of the show function).
Number values are members of the Num type. Mars numbers are represented internally as binary floating-point numbers; as such, the members of this type form a subset of the real numbers. The numbers have at least 64-bit “double” precision. The usual caution about floating-point rounding errors and imprecision applies.
The most common representation is the IEEE 754 binary64 format (commonly known as “double”), but the implementation is allowed to use any binary floating-point format that satisfies the following criteria:
Note
The IEEE 754 binary64 format has a 52-bit mantissa. It represents values up to 2^53 via an implicit 1 bit.
This means that the implementation must provide at a minimum a 64-bit “double” format; any larger binary floating point format is also acceptable, and programs should be written to allow for variations in floating-point precision. The implementation is not permitted to represent values as 32-bit floats (the equivalent of the C float type). The above rules mean that:
Unlike in some floating-point systems, there is no distinction between 0 and -0. The two values are considered equal under all circumstances, and -0 is displayed without a minus sign.
Warning
An overflow (a value with a larger magnitude than the largest representable value, which is at least 2^1023) will result in undefined behaviour. While the implementation is likely to generate infinity values, the specifics of dealing with such values are unspecified and this behaviour should not be relied upon.
Number values can be created using number literal expressions. Also, many built-in functions produce number values (such as all of the arithmetic functions). The string representation of a number is an ASCII decimal representation, preceded by a hyphen if the value is negative. Integers (whole numbers) are always displayed without a decimal point or fractional component. Other numbers are displayed with a decimal point as a number literal; the implementation is allowed to display numbers with a large or small magnitude with the “e” exponent notation, such as 4.2e+18 (which is not a valid number literal).
Array values are members of the Array type family. Arrays in Mars are extensible arrays (sometimes known as “vectors”), the elements of which may be accessed in constant time. An array is a collection of elements that has a specific order and allows duplicate elements. Array types are parameterised by a type variable a; this denotes the type of all of the elements. For example, the type Array(Num) permits only elements of type Num.
Array elements are typically referred to by index. The index is an integer, where the first element of the array has index 0, and each subsequent element has an index 1 greater than the previous. The length of the array is the number of elements contained within it. The index of the last element of the array is therefore length-1. It is not meaningful to talk about elements with a negative index, or elements whose index is greater than or equal to length.
Individual array elements can be modified in constant time, regardless of the element’s position in the array. Furthermore, new elements can be added to the end of the array in amortised constant time (typically, this is implemented by allocating more memory than the array requires, and having the array occasionally resize by doubling the amount of memory allocated).
Warning
Constant-time array updates are currently only possible by importing the impure library module, and are considered to be an unofficial part of the language.
Array literals may be used to build array values. Built-in functions are also available to create and manipulate arrays. The string representation of an array is the same as the array literal syntax: a comma-separated list of the string representation of each element, enclosed in square brackets.
Function values, or closures, are members of the -> and ->io type families. As Mars is a functional programming language, all functions are first-class and may be treated as ordinary values. A function value may be called, with a fixed number of arguments being supplied, and a resulting value will be returned. Some functions may also perform I/O effects (such as printing) when they are called.
If the -> type constructor is given n arguments, the first n-1 arguments are the types of the function parameters, while the final argument is the type of the function result. For example, values of type (a, b) -> c are functions which accept two arguments of types a and b respectively, and produce a result of type c.
The data carried by a function value is opaque, but it is likely to include the code that specified the original function. Because Mars supports closures, function values may also contain other data values. For example, the expression add(1, ...) creates a function value that not only includes the code for the add function, but also the value 1 which will be needed when the function is called.
Functions may only be created by being declared at the top level, or through currying.
The string representation of a function is implementation-defined. Because functions are considered opaque, it should not technically be obtain any information about a function other than by calling it. However, implementations may display information about the function object, such as its original name or memory location, in order to help identify the function. The string should therefore not be used in computations (only for debugging or display purposes).
There are two distinct type constructors for functions: -> and ->io. These distinguish between functions declared as “pure” (without the io keyword) and those declared with the io keyword. These two types are totally incompatible: it is not possible to convert an I/O function to a pure function, and a pure function cannot be used in a place where an I/O function is expected.
Note
It is possible to explicitly convert a pure function to an I/O function using the toio functions in the iofuncs module.
When a type is declared using the type keyword, a set of values is created for that type. The values that inhabit each user-defined type are totally distinct from those of any other type. This is because a) each user-defined type declares a unique set of data constructor names, and b) each value of a user-defined type belongs to some constructor of that type.
As described in Native types, each user-defined type T consists of a set of one or more data constructors c0, ..., cn, and each constructor ci has a unique name, and a sequence of zero or more fields fi0, ..., fim (where m varies from one constructor to another). Each field fij has a type tij. Every value of type T belongs to a particular data constructor ci, and has a value of type tij for each field of that constructor. User-defined types may also be parameterised; the type parameter is used to specify the types of various fields.
For example, the List type from the prelude is defined as:
type List(a):
Cons(head :: a, tail :: List(a))
Nil
This type has two data constructors, Cons and Nil. This means that every value of type List(a) belongs to either the Cons or Nil constructor. Values that belong to the Cons constructor contain two sub-values: one value of type a and one value of type List(a). There is only one value that belongs to the Nil constructor: Nil (as that constructor has no fields).
Values of a user-defined type can be created by calling the constructor function of the same name as one of the constructors; this will determine which constructor the value belongs to and also initialise all of the fields. Values of parameterless constructor (one declared without parentheses) cannot be created; they already exist as global values of the same name (for example, Nil is a global constant which is the only value that belongs to the constructor Nil).
The switch statement can be used to determine which constructor a particular value belongs to, and also access the value’s fields. The assignment statement and field reference expression can also be used to access the value’s fields. Two values of the same type are considered equal if a) they both belong to the same constructor, and b) all of their corresponding field values are equal (see eq).
The string representation of a value of a user-defined type is the same as the construction syntax: for parameterless constructors, it is just the constructor name. Otherwise, it is the constructor name, followed by a comma-separated list of the string representation of each field, enclosed in parentheses.