Documentation

Hello World!, Comments
Numbers, Strings, Booleans, Labels
If, Cond
Variables, Functions, Where
Partial Application, Pipe Operator
Lists, Arrays, Sets, Dicts, Tuples, Objects
For, When
Lists, Defs
Tail-Call Optimization, Concatenation
Basic Types, Data Structures, Function Comparison, Label Comparison
Via With, Via Update Functions
IO:
Output Commands, Debug Input Commands
Import Statements Export Statements
Errors, Exceptions / Throw, Try / Catch, Requires
Special Field Wrappers


Getting Started:

See the install page for instructions on downloading, building, and running Pointless.

Hello World!

This code implements the classic "Hello World!" program in Pointless:

output = println("Hello World!")

Comments

Comments in Pointless begin with two dashes '--' and span the rest of the line. Comments can be placed on the same line as code (following the code), or on their own line, as seen below. A long bar of dashes can be used to separate blocks of code and provide documentation for functions.

-----------------------------------------------------------
-- check whether a number is even -- returns true for even
-- ints, false for odd ints or non-int numbers

isEven(n) = n % 2 == 0 -- even ints equal zero mod 2

Basic Types:

Pointless has four basic types: numbers, strings, booleans, and labels.

Numbers

All numbers in Pointless use double-precision floating-point representation. Pointless supports a number of common mathematical operations.

-- numerical operations:
a = 17
b = 3.4

a + b   -- addition
a - b   -- subtraction
a * b   -- multiplication
a / b   -- division
a % b   -- modulus
a ** b  -- exponentiation

-- inequality operations:
a < b   -- less-than
a > b   -- greater-than
a <= b  -- less-than or equal-to
a >= b  -- greater-than or equal-to

Strings

In Pointless, strings are the fundamental type for representing pieces of text -- Pointless has no character type. Two strings can be concatenated into a new string using the + operator.

-- an empty string
a = ""

-- an non-empty string
b = "I lost the game"

-- string addition
c = b + ", again!"

Booleans

Booleans in Pointless are represented with the keywords 'true' and 'false'.

-- booleans
a = true
b = false

-- boolean operations:
[a and b, a or b, not a]

Labels

Labels in Pointless are like symbols in Lisp -- they can be used much like enumeration values in other language, except that the enumeration doesn't have to be explicitly declared. Label names start with a capital latter, and can contain alphanumeric characters.

-- some labels
a = North
b = Level3
c = C

Conditionals:

Pointless has two conditional constructs if and cond, which are used to select the value of one of several expression based on boolean conditions.

If

If conditionals follow the form if [condition] then [then expression] else [else expression]. If expressions can be broken into multiple lines for readability. If expressions can be nested, as in the second example below, where the else of the first conditional is a second conditional. Note that the expressions in the then and else branches are not evaluated unless the branch is selected by the condition.

-- a simple conditional
getSign(n) = if n >= 0 then Positive else Negative

-- a nested conditional
getSignZero(n) =
  if n > 0 then Positive
  else if n < 0 then Negative
  else Zero

output = range(-3, 3) |> map(getSignZero) |> println

Cond

Cond conditionals consist of one or more cases of the form case [condition] [result]. When a cond is evaluated, the conditions are evaluated in-order; when a case is found with a true condition, the cond conditional returns the value of the result expression for the case. A cond conditional can also include a default case of the form otherwise [result] (the default case must come last in the list of cases). The value of the default case will used if no case conditions are true. If no conditions are satisfied and the cond has no default case, the conditional will raise an error.

As in if conditionals, the result expressions of cases are evaluated lazily.

In the example below, the first cond returns a label based on a number; the second cond returns a string based on a label value.

-- a cond statement with a default case
getSignZero(n) = cond {
  case n > 0 Positive
  case n < 0 Negative
  else Zero
}

-- a cond statement with no default
getSignSymbol(sign) = cond {
  case is(Positive, sign) "+"
  case is(Negative, sign) "-"
  case is(Zero, sign)     "0"
}

output = getSignZero(-4) |> getSignSymbol |> println

Definitions:

As seen in the previous section, conditionals in Pointless are themselves expression -- they evaluate to a value. A Pointless program is made up entirely of expressions and definitions -- Pointless does not have explicit execution flow, unlike imperative programming languages. Furthermore, every expression in a program must be part of a definition -- Pointless doesn't allow functions to be called without using the resulting value as many other languages do.

All definitions occur in a scope. Constructs like functions, where expressions, and objects create new local scopes for their parameters / definitions. Local scopes form closures over non-local variables.

Variables

Variable definitions take the form [variableName] = [expression]. Variable names start with a lowercase letter and can contain alphanumeric characters. A variable name can only be defined once in a given scope.

-- some variable definitions
n = 1024
isEven = n % 2 == 0
isDiv3 = n % 2 == 0

Functions

Function definitions take the form functionName(param1, param2, param3) = [functionBody] (for any number of parameters >= 1). Parameters must be valid variable names (alpha-numeric, beginning with a lower-case letter).

double(n) = n * 2

Functions may be defined recursively, as in the factorial example below.

-- an (inefficient) recursive factorial implementation

factorial(n) = if n == 0 then 1 else n * factorial(n - 1)

Functions can also be declared anonymously (lambda functions), shown below:

-- a single-parameter lambda function
n => n + 1

-- a multi-parameter lambda function
(a, b) => a + b

Under the hood, function definitions de-sugar to assignment to a lambda function, such that the following two lines are equivalent:

double(n) = n * 2
double = n => n * 2

Writing, (and more importantly, reading) programs in Pointless is often easiest when functionality is divided among many small functions and definitions. For example, the following code:

output = longestPair |> println

longestPair =
  range(1, 99)
  |> map(getLengthPair)
  |> argmax(at(0))
 
getLengthPair(n) = (length(getSeq(n)), n)

getSeq(n) =
  iterate(step, n)
  |> takeUntil(eq(1))
 
step(n) =
  if n % 2 == 0 then round(n / 2) else n * 3 + 1

is preferred over the equivalent condensed code:

output = range(1, 99)
  |> map(n =>
    (length(
      takeUntil(eq(1), iterate(n =>
        if n % 2 == 0
        then round(n / 2)
        else n * 3 + 1, n))), n))
  |> argmax(at(0))
  |> println

Where

A where expression can be used to create new scopes and define local variables. These expressions take the form expression [expression] where [definitions]. A where expression with a single definition (an intermediate value used in the function) is shown below.

volume(radius, height) =
  base * height
  where base = pi * radius ** 2

Alternatively, a where clause can contain a curly-bracketed group of multiple definitions.

distance(x1, y1, x2, y2) = sqrt(dx ** 2 + dy ** 2) where {
  dx = x1 - x2
  dy = y1 - y2
  sqrt(n) = n ** .5
}

Calling Functions:

Functions call syntax uses parentheses around group of comma-separated argument expressions:

mul(a, b) = a * b
output = println(mul(2, 3))

Calling a function with more arguments than it has parameters results in an error.

Partial Application

When a function is called with fewer arguments than its number of parameters ("partial application"), a new function object is created which will accept the remaining function parameters, while storing the already-supplied argument values. In the example below, triple is a function which takes one parameter (b). When triple is called, it returns value of the original function mul called with the stored value for param a (3) and the new value for b (2).

mul(a, b) = a * b
triple = mul(3)
output = println(triple(2))

Pipe Operator

Function calls can also be expressed using the function-pipe operator |>, where a |> b is equivalent to b(a), and a |> b |> c is equivalent to c(b(a)), and so on. Thus, the following call:

output = println(range(3))

could instead be expressed as like this:

output = range(3) |> println

Chaining function calls through the pipe operator can increase the legibility of otherwise nested calls. For example, the following code:

output = println(map(mul(2), range(3)))

could instead be written as a sequence of transformations:

output = range(3) |> map(mul(2)) |> println

Data Structures:

Pointless has a number of built-in data-structures -- that is, types which contain other types within them. These data-structure types are: lists, arrays, sets, dicts, tuples, and objects. The examples below demonstrate the basic methods for creating and interacting with data-structure in Pointless. See the prelude api docs for many more data-structure-related functions.

Lists

A list is used to store an iterable sequence of values. Unlike arrays, in which elements can be accessed through numerical indecies, list elements can only be accessed through iteration. Lists are created through the syntax below: a square-bracket-enclosed comma-separated sequence of values. An empty list is represented with empty brackets [], or equivalently with the label Empty. The function head returns the first value in a list, the function tail returns a list of all values after the first. Calling head or tail on an empty list results in an error.

-- a list of numbers
numbers = [1, 2, 3]

a = head(numbers) -- 1
b = tail(numbers) -- [2, 3]

-- empty list
empty = []

Two lists can be concatenated with the concat operator '++' to form a new list. Note that lists (as well as other data-structures) can contain different types of values at once.

a = [] ++ []                  -- []
b = [1] ++ [2, "three"]       -- [1, 2, "three"]
c = [false] ++ ["asdf"] ++ [] -- [false, "asdf"]

The concat operator can be used in recursive functions to build lists procedurally. The collatz function below creates a list containing a collatz sequence starting at a given number:

-- end sequence once 1 is reached 
-- otherwise current value is the start of the output sequence,
-- along with the sequence starting at the next value

collatz(n) =
  if n == 1 then [1]
  else [n] ++ collatz(step(n))

step(n) =
  if n % 2 == 0 then n / 2 else n * 3 + 1

In addition to producing lists, functions can take lists as input and process them recursively, like the take function below, which takes the first n elements from an input list (or fewer if the input list contains fewer than n elements):

take(n, list) = cond {
  case n == 0 []
  case isEmpty(list) []
  -- take one value, and reduce the number of values left to take by one
  else [head(list)] ++ take(n - 1, tail(list))
}

Arrays

Arrays, like lists, contain sequence of elements, represented by square-bracked enclosed space-separated sequence of values. Unlike lists, arrays have a fixed size (that is, they cannot be extended or concatenated). Their elements are accessed through the index syntax below. Pointless also includes a special syntax for nested two-dimensional arrays, also seen below:

-- an array of numbers
a = [1 2 3]

x = a[0] -- 1 (arrays are zero-indexed)

-- a two-dimensional array of numbers
b = [1 0 -1]
    [2 0 -2]
    [1 0 -1]

y = a[1][2] -- 2 (row 1, column 2)

Sets

A set is an unordered collection of unique values. Uniqueness is determined according the language's equality rules. The operator in can be used to see if a set contains a given value.

e = toSet([]) -- an empty set

-- an set of labels
directions = {North, East}

x = North in directions -- true
y = South in directions -- false

Dicts

A dict (dictionary) is an unordered collection of key / value pairs, where each key is unique. As with sets, uniqueness follows equality rules. The operator in is used to check whether the keys of a dict contain a given key value. The value associated with a given key in a dict can be accessed with the syntax dict[key] as seen below. Note that attempting to look up a non-existing key in a dict results in an error.

-- an empty dict
e = {}

-- an dict of from strings to numbers
values =
  {"pawn": 1, "knight": 3, "bishop": 3, "rook": 5, "queen": 9}

x = "pawn" in values -- true
y = "king" in values -- false

pawnVal = values["pawn"] -- 1

Tuples

A tuple is a ordered group of values. Unlike a list or array, the elements in a tuple normally serve different purposes, and are often different types. The values in a tuple are accessed through destructuring assignment, as shown below:

-- an two-element tuple
point = (3, 4)

-- destructuring assignment
(x, y) = point

Tuples can also have labels -- labeled tuples can be created using the function wrap (to make a single-element tuple with a given label) or the function wrapTuple (to make a labeled tuple from an unlabeled tuple). Pointless also has special syntax for creating labeled tuples, where SomeLabel(val) is equivalent to wrap(SomeLabel, val) and SomeLabel(valA, valB, valC) is equivalent to wrapTuple(SomeLabel, (valA, valB, valC)) for any number of values >= 2:

-- a tree structure made with labeled tuples
tree = Branch(Leaf(1), Branch(Leaf(2), Leaf(3)))

-- making a tuple by calling wrap with a label stored in a variable
label = Leaf
l = wrap(label, 0) -- Leaf(0)

The label of a tuple can be checked using the is function:

-- a tree structure made with labeled tuples
tree = Branch(Leaf(2), Leaf(3))

x = is(Branch, tree) -- true
y = is(Leaf, tree)   -- false

Objects

An object is a set of definitions -- as in where expressions, these can be variable definitions or function definitions. The fields (definitions) within an object can be accessed through using the syntax objectValue.fieldName show below:

-- an object
pet = {
  age = 4
  name = "rufus"
}

output =
  format("{}, age {}", [pet.name, pet.age])
  |> println

Like tuples, objects can also have labels. Labeled objects can be created using the function wrapObject, which makes a labeled object from an unlabeled object. Pointless also has special syntax for creating labeled objects, where SomeLabel {definitions} is equivalent to wrapObject(SomeLabel, {definitions}). The label of a tuple can be checked using the is function:

-- a labeled object
lilah = Cat {
  age = 12
  name = "lilah"
}

x = is(Dog, lilah) -- false
y = is(Cat, lilah) -- true

Comprehensions:

List comprehension syntax provides another way to create lists in Pointless. A list comprehension expression takes the form for [variable] in [values] [body], where the body of the comprehension is either a yield statement or another comprehension. The yield statement marks the expression used to generate the elements in the resulting list (the yield keyword is necessary to disambiguate nested comprehensions). To generate the new list, the given variable name is assigned to each value in the values list, and the body expression is evaluated to create new elements in the output list:

For

-- starting with a list of numbers 1 through 10, generate
-- a new list containing the squares of these numbers

squares = for n in range(1, 10) yield n ** 2

-- generate a list of pairs
-- [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2) ...]

pairs =
  for a in range(0, 2)
  for b in range(0, 2)
  yield (a, b)

When

Comprehensions can also include when expressions of the form when [condition] [body], where the body expression is evaluated only when the condition is met -- otherwise no elements are returned for that iteration of the comprehension. When expressions can be used to filter the output from a comprehensions:

-- generate a list of pairs
-- [(0, 0), (1, 0), (1, 1), (2, 0), (2, 1), (2, 2)]

pairs =
  for a in range(0, 2)
  for b in range(0, 2)
  when a >= b
  yield (a, b)

List comprehensions are syntactic sugar -- the precise semantics of for, when and yield can be understood by looking at canonical forms that the parser translates these expressions to:

for var in iter body
-- becomes
concatMap(var => body, iter)

when test body
-- becomes
if test then body else []

yield body
-- becomes
[body]

Laziness:

Expressions in Pointless are normally evaluated eagerly. There are exceptions to this rule, like the branches of conditional expressions, as described previously. There are two other important instances where the language introduces laziness: lists and definitions.

Lists

Lists introduce laziness in two ways: first, the elements in a list expression are not evaluated until the elements themselves are accessed (using the head function). Second, when two lists are concatenated, the right-hand value in the concatenation is not evaluated until it is accessed (when traversing the list using the tail function). As a result of this, functions with recursive calls on the right-hand side of a concatenation expression can be used to generate infinite lists, as seen in the example below, which continuously generates new list elements and prints them to the console.

repeat(value) = [value] ++ repeat(value)

output =
  repeat("How long till we get there?")
  |> enumerate
  |> printLines

The Pointless prelude contains many functions that can process infinite lists -- for example the take function, which can be used to make finite list from an infinite list:

zeroToNine = iterate(add(1), 0) |> take(10)

Note that list comprehensions can also be used to process and generate lazy lists.

Defs

Like lists, the values of definitions are also calculated lazily. Specifically, when a new set of definitions is encountered (in the global scope, a where clause, an object, etc), all definitions are registered in the new scope before any are evaluated. As a result, the order of definitions in a program does not matter, as shown in the example below:

a = 1

output = [a, b, c] |> println

b = 2
c = 3

Note that unlike other definitions, arguments to functions are calculated eagerly.

Normally, accessing the value of a circularly-defined variable will result in a call-stack overflow. However, lazy definitions allow some types of recursive structures to be defined -- for example recursive objects. While it is possible to use these recursive structures in some situations (as shown below), using them with other operations, like the show function, will make the program enter in infinite loop:

x = {myself = x; value = 123}

a = show(x.myself.myself.value) -- "123"
b = show(x) -- loops infinitely

Performance:

Notes on two important performance considerations when writing programs in Pointless.

Tail-Call Optimization

Pointless performs tail-call optimizations during program execution -- that is, a function which returns the result of another function call does not extend the call-stack when performing this inner call. The following code, for example, is not in tail-call form and cannot be optimized, since the addition step is performed after the inner call returns. This function would create a call-stack overflow if called on a large list.

sum(list) =
  if list is Empty then 0
  else head(list) + sum(tail(list))

output = sum(range(1000)) |> println

On the other hand, the following function performs the addition step before making its recursive call, and will thus be tail-call optimized, making it suitable for input lists of arbitrary length:

sumHelper(result, list) =
  if list is Empty then result
  else sumHelper(head(list) + result, tail(list))

sum = sumHelper(0)

output = sum(range(1000)) |> println

Concatenation

During concatenation, Pointless makes a copy of the left-hand list being concatenated. As a result, the concatenation operation has a time complexity that is linear in the length of the left-hand list. This also means that the concatenation operation only works when the left-hand list has a finite length.

On the other hand, the right-hand side list in a concatenation remains lazily evaluated. This means that concatenation operations with a recursive call on the right-hand side are still in tail-recursive form, and can be used to generate infinite, lazy lists, as shown in the repeat function below, which creates an infinite list of a given value, repeated:

repeat(value) = [value] ++ repeat(value)

In situations where lazy concatenation is needed with respect to both the left-hand and right-hand lists, use the concat function from the prelude.


Equality:

Definitions of equality for values in Pointless. Note that hashes for sets and dicts keys follow equality rules: (hash a equals hash b if and only if a == b).

Basic Types

Basic types (numbers, booleans, strings, and labels) can be compared using the equality operators == and != (note that comparing non-integer numbers using these operators may fail due to floating-point inaccuracies).

123 == 123 -- true
123 != 4   -- true

Data Structures

Data structures (lists, arrays, sets, dicts, objects, and tuples) can also be compared with the operators above. In this case, equality is determined structurally: for example, two arrays are equal if and only if they contain the same values in the same order.

Function Comparison

Functions are compared according to their location in memory, as demonstrated below:

incA(n) = n + 1
incB(n) = n + 1

incA == incA -- true
incA == incB -- false

Label Comparison

As seen previously, the == operator can be used to compare labels Additionally, is can be used to check the labels of tuples and objects, and to determine the type of other values in pointless. For example, the expression is(PtlsNumber, value) can be used to check if a value is a number, is(PtlsBoolean, value) can check booleans, and so on. Note that is(PtlsLabel, value), is(PtlsTuple, value), and is(PtlsObject, value), will also return true for any labels, tuples, and objects, respectively, regardless of their labels. All of the example expressions below will return true:

is(PtlsNumber , 123)                 -- number
is(PtlsString , "asdf")              -- string
is(PtlsBool   , true)                -- boolean
is(PtlsLabel  , Foo)                 -- label
is(Foo        , Foo)                 -- label
is(PtlsFunc   , sum)                 -- function
is(PtlsBuiltIn, {}.!getDelKey)       -- built-in
is(PtlsSet    , {1})                 -- set
is(PtlsDict   , {"one": 1})          -- dict
is(PtlsList   , ["a", "b"])          -- list
is(PtlsArray  , [0 0 0])             -- array
is(Baz        , Baz {x = 0; y = 2})  -- labeled object
is(PtlsObject , Baz {x = 0; y = 2})  -- labeled object
is(PtlsObject , {x = 0; y = 2})      -- unlabeled object
is(Bar        , Bar(1, 2))           -- labeled tuple
is(PtlsTuple  , Bar(1, 2))           -- labeled tuple
is(PtlsTuple  , (1, 2))              -- unlabeled tuple

Persistent Updates:

In Pointless, arrays, sets, dict, objects, and tuples are persistent, immutable types -- that is, once created, their values cannot be changed. Instead, Pointless provides several ways to create new data-structures based on old ones, depending on their types.

Via With

With expressions can be used to create new arrays, dict, and objects from old ones. These expressions take the form [value] with [updateDefinitions]. In an update definition, the character $ is used to represent the structure being updated. With expressions work by first cloning the original structure, and, for each update definition, updating the value of some index or field in the original new. The value returned by the with statement is the value of cloned structure once all updates have been applied. In the below example, the newValues is a clone of values with its first element (zero index) set to the value zero.

values = [1 2 3]
newVals = values with $[0] = 0 -- [0 2 3]

With expressions can contain multiple update definitions, as in the example below. As with where expressions, with expressions with multiple definitions must wrap these definitions in curly brackets. The below with statement returns a clone of the original array with the elements at indecies zero and one swapped. Note that the right hand sides of the update definitions are calculate before the updates are processed -- which allows the swap to occur without using a temporary variable.

values = [4 10]

newVals = values with { -- newVals = [10 4]
  $[0] = values[1]
  $[1] = values[0]
}

Note that updates are processed in order from top to bottom, which is relevant if a later update overrides an earlier update.

The following example demonstrates the nested update of an object containing a dict and an object in its fields.

structure = Data {
  foo = [-1 0 -1]
  bar = {"green": false}
}

-- newStruct = Data {foo = [-1 0 1]; bar = {"green": false, "blue": true}}
newStruct = structure with {
  $.foo[2] = 1
  $.bar["green"] = false
  $.bar["blue"] = true
}

Note that Pointless uses efficient representations for persistent structures so as to avoid the cost of fully copying structures when performing updates.

Via Update Functions

The Pointless prelude provides the following functions for adding elements to sets, and deleting elements and key / value pairs from sets and dict, respectively. Note that deleting a non-existing element or key from a set or dict has no effect.

addElem({1, 2}, 3)        -- {1, 2, 3}
delElem({1, 2}, 2)        -- {1}
delElem({1, 2}, 0)        -- {1, 2}
delKey({Foo: "bar"}, Foo) -- {}

IO:

Notes on the input / output capabilities of Pointless.

Output Commands

Unlike other programming language, which use functions to send information from a program to the outside system, Pointless programs communicate their output through a sequence of output commands. To accomplish this, Pointless designates a special variable, output, from which output commands are read when an program is run. Some programming languages have a main method, which serves as the entry-point for executing the program -- in Pointless, the output variable also serves as an entry-point, where the value of the output variable is the output from the program.

Pointless expects the output variable to be defined as a list of commands. Commands can be either labels or labeled values -- the two output commands which Pointless currently supports are IOPrint(value), which directs the Pointless interpreter to print the string value, and IOClearConsole, which clears the console. The top-level source file for a Pointless program must define an output variable, and the value of output must be a valid list of commands -- programs which do not meet these requirements will cause an error at runtime.

Since lists in Pointless are processed lazily, the sequence of output commands defined by a program will also be handled lazily. This means that, while programs must define a single entry-point for their output, that output is still handled as the programming running, rather than blocking until the program terminates.

IOPrint(value)
IOClearConsole

To avoid interacting with these output commands directly, the Pointless prelude defines the following functions to help generate output sequences. The examples page demonstrates the many uses for these functions.

print(value)       -- generate command sequence to print the string rep for value
println(value)     -- generate command sequence to print value with a newline
printFrame(value)  -- generate command sequence to clear console and print value with newline
printLines(values) -- print each element in a sequence on a separate line

Because output sequences are made up of normal Pointless values, they themselves can be manipulated and visualized. In the example below, the code printLines(range(3)) generates a command sequence to print the numbers zero through two -- the code then calls the println function again to print a string representation of these commands. Running this code thus provides a visualization of a typical output command sequence.

output = printLines(range(3)) |> println

Debug

Sometimes when debugging, the task of integrating debugging output into the larger output stream of the program is cumbersome. Pointless therefore provides a special function debug which does the following:

  1. Takes a single value as an argument
  2. Prints show(value) to the console
  3. Prints the code location where the value was created
  4. Returns the value

Input Commands

At present, Pointless provides two input sources -- lines read from stdin, and random numbers. These inputs are accessible through special fields on the IO label, shown below:

IO.!getLines -- a lazy list of input lines
IO.!getRand  -- a random decimal value (between 0 and 1)

As with other special fields, the prelude has definitions for accessing these inputs, including the readLines and several functions for generating random numbers / choices.

readLines         -- read lines of input lazily
randFloat(n)      -- get random float in range(n)
randRange(a, b)   -- get random entry from range(a, b)
randChoice(elems) -- get random elem from collection

While the set of input choices is currently limited, the existing input sources can be easily processed using existing functions. For example, the following code reads lines from stdin, and prints the length of each line.

output = readLines |> map(length) |> printLines

Imports:

Import statements are used to share code across multiple source files.

Import Statements

Import statements have the form import "filePath" as [name]. By default, import statement will load all of the definitions from the given source file and return these in object, which is then stored in the specified name. All import statements in a program must come before any variable or function definitions. Circular imports are allowed (two files can both import each other, for example), although this will make the import objects themselves contain circular references.

import "chart.ptls" as chart

Export Statements

To avoid importing helper methods, a source file can specify a set of names that are importable using an export statements of the form export {name1, name2, name3} for any number of names. When a file with an export statement is imported, only the specified names will available to the importing file. Note that an export statement (if a file contains one) must come before all imports and definitions.

export {
  foo,
  bar,
  baz, -- trailing comma ok
}

Errors and Exceptions:

Pointless programs use both errors and exceptions to signal erroneous states -- however, these mechanisms arise in different scenarios and are handled in different ways.

Errors

Errors in Pointless are caused by invalid code which the interpreter is unable to handle. Some issues which result in errors include:

When an error occurs in a Pointless program, the program stops running and reports the error. Programs cannot recover from errors.

Errors can sometimes be prevented before they occur. Functions can be adapted to check for certain erroneous conditions before performing an operation that could lead to an error. For example, indexing a dict with a key that the dict does not contain will result in an error -- this could be prevented by wrapping a dict access in a conditional which check that the dict contains the required key before attempting to index the dict, and falling back to some other behavior if the key is not present:

if key in dict then dict[key] else somethingElse

Similar techniques could be used to prevent other errors including:

Exceptions / Throw

Unlike errors, exceptions in Pointless are explicitly thrown, and must be caught and handled. Exceptions are thrown using expressions of the form throw [value]. Exceptions are useful in situations where indicating error states using return values would significantly complicate a function's API (see the self-hosting tokenizer example). However, exceptions can introduce their own complexity, and should primarily be used internally (that is, not part of the API of exported functions). An unhandled exception (an exception which isn't handled anywhere in a program) results in an error.

Try / Catch

Exceptions are handled using try / catch expressions of the form try [body] catch [conditionFunc] [handlerFunc]. If an exception occurs while evaluating the body expression, then the condition function is called on the value of the exception. If the condition function returns true, then the handler function is called on the exception value and the result is returned as the value of the entire try / catch expression. If the condition function returns false, then the exception continues to propagate. If no exception is thrown in the body expression, then its value is returned as the value of the entire try / catch expression.

The following code from the self-hosting tokenizer example catches and prints tokenizer errors:

output =
  try
    new(text)
    |> getTokens
    |> convert
    |> map(showTok)
    |> eager
    |> printLines

  catch is(TokenizerError)
    err => println(unwrap(err))

In the code above, the function eager is used to force complete evaluation of the tokens list in order to catch errors before it is returned from the try / catch expression.

Requires

Requires statements are similar to assertions in other programming languages -- they provide an easy way to check that a condition is met and throw an error if it is not. Unlike assertions, requires statements are attached to a specific expression, and take the form [expression] requires [condition], where the condition is checked before the expression is evaluated. This allows functions to verify preconditions before they run, as in the following function, which returns the last element in a list after checking that the list is not empty.

last(list) = head(reverse(list)) requires list is PtlsList

Prelude:

The prelude is the set of files which contain the definitions for the Pointless standard library. API documentation for all prelude functions can be found here.

Special Field Wrappers

Values in Pointless have a number of special fields of the form .!get[FieldName], for example "123".!getInt (get the integer value of the string), {false}.!getAddElem (get the cloned set with the added element), and so on, which are used to expose built-in functions of Pointless types. The prelude contains wrapper functions for accessing this functionality, for example toInt and addElem, which wrap access to the fields above. Avoid accessing special fields directly -- use the provided wrapper functions instead (the APIs of the wrappers are more stable than those of the fields themselves).