Tutorial¶
Introduction¶
Welcome to xs, an functional concatenative array language inspired by kdb+/q, forth, and others. The xs interpreter is written in OCaml and released into the public domain by yours truly. xs might initially strike you as unusual, strange, and perhaps even deliberatly unfriendly (especially if you don’t have any prior experience with array languages); but I guarantee that in no time at all reading and writing xs will become second nature!
If you haven’t already, check out the installation instructions in order to get a working environment. After the installation completes, start the interpreter by simply running:
$ xs
You should see a prompt similar to the below in which you can enter commands:
Public domain interpreter for the xs language. Created by smabie
(sturm@cryptm.org). For more information check out the project's github at
http://github.com/smabie/xs
xs>
Try adding two numbers:
xs> 2+5
0: 7
xs is a concatenative (or stack based) language in which all operations are parsed from right-to-left.
You might be wondering at this point how the +
operator works
since it’s being used in an infix fashion. The answer is that xs
allows you to define special operators that switch the operator with
the position of it’s left adjacent value. So internally, the
interpreter is evaluating + 2 5
. Now clear the top most element:
xs> drop
xs>
From now on, we will assume that you are starting from a clean stack, though you can keep old values on the stack if you so choose.
Since everything is evaluated from right-to-left, all operators have the same precedence:
xs> 2*3+1
0: 8
In order to turn an operator into a normal function, we can use the
.
operator to apply the function:
xs> +. 3 2
0: 5
You might be wondering how this works, since the interpreter will
switch the +
with the .
and then evaluate the .
. For
example if we just try:
xs> + 3 2
(Invalid_argument "index out of bounds")
We get an error. The interpreter is trying to switch the +
operator with the value to its left, but there is no value to its
left, so we receive an error. One might think that the same thing
would happen with the .
operator. In order to solve this problem,
xs will conditionally quote the left-most value if it is an
identifier. So +. 3 2
will turn into:
. `+ 3 2
A backtick followed by an identifier represents a quoted value, which
is in many ways similar to a string, but cannot contain spaces. What
follows is the sequence of operations taken to evaluate +. 3 2
:
The interpreter sees the integer
2
and pushes it onto the stack. The stack now looks like:0: 2
The interpreter sees the integer
3
and pushes it onto the stack. The stack now looks like:1: 2 0: 3
The interpreter now sees the identifier
.
. It looks up the identifier to see if it has a definition in the current scope. The definition is found and the identifior references an operator. Now xs looks at the value to the left of.
. It sees another identifier, which means it should push it’s quoted form onto the stack:2: 2 1: 3 0: `+
Now xs goes back to the
.
and calls the function associated with its definition; in this case, the apply function. The apply function now expects one argument, an identifier or a function. If it’s a function literal (a function that has been pushed onto the stack), it calls it. If it’s an identifier it looks up the value (which must be a function), and calls it. Note that when a quoted value bound to a function is applied by.
, it does not matter if the function is an operator or not. Finally, the stack looks like:0: 5
Comments¶
Comments are denoted by the !
symbol and span until the end of the
line:
xs> 3+2 ! this is a comment
0: 5
Function and Variables¶
We could define a function that multiplies the top level value on the
stack by 2
with the following:
xs> mul2:(2*)
We can now call it with 5
as the parameter:
xs> mul2 5
0: 10
Parenthesis are used to define a function and the :
operator is
used to assign a value (including a function) to an identifier:
xs> x:3
xs> mul2 x
0: 6
Note that we don’t have to name the function in order to call it:
xs> (2*). 5
0: 10
If .
is given a function it will be evaluated, if it’s given an
identifier, it looks up the definition and pushes it onto the stack if
it’s not a function; if it is, it will called as well. For example:
xs> x:5
xs> x.
0:5
You can also use .
on itself:
xs> x:5
xs> ..`x
0:5
We need to use the .
operator because when a function literal in
encountered, it is simply placed on the stack instead of being
evaluated. When bound to an identifier on the otherhand, the function
is automatically called. Note that we did not define any argument name
for the function, and instead used what is called point-free style. If
we wanted to explicitly bind the parameter to a name, we could write:
xs> (x:; 2*x). 5
0: 10
xs evalues expressions from from right-to-left, but lines can be broken up with a semicolon. For example:
xs> (2+2; 3+)
0: 7
the :
operator pops the value off of the stack, but we can use the
~
operator if we wish to only bind and leave the stack unchanged:
xs> sq:(x*x~)
xs> sq 5
0: 25
This operator is often useful for intermediate or temporary variables that are used multiple times in an expression.
Operators¶
In xs, operators are not special and can be defined by the user. In order to define an operator, we simply use curly brackets instead of parenthesis to enclose our function definition:
xs> add:{y:x; x+y}
xs> 3 add 4
0: 7
While this will work for literals, a problem arises when trying to
call add
on a variable:
xs> x:3; x add 4
(Failure "+ applied on invalid types")
Behind the scenes, x
is being passed in as the quoted literal
`x
, which is invalid. In order to solve this problem, we first
need to dereference x
before passing the value to the +
operator:
xs> add:{y:x:..; x+y}
xs> x:3; x add 4
0: 7
The ..
isn’t a unique operator, we’re simply using the regular
.
operator on itself in order to use the prefix form of it.
Notice how in our first statement we bound the variable x
to the
first-most value (after being dereferenced by .
) on the stack, and
then the variable y
to the second-most value. This turns out to be
a common operation in xs, as the first line of many functions first
set up their variables (often times, using explicit variables can be
clearer than a point free style). :
and ~
therefore supports
binding multiple values at once:
xs> ([`x`y`z]):1 2 3
xs> x*y-z
0: -1
What is happening here is that :
is taking a function that
produces a list as its first argument and binding them, in order, to
values on the stack. The list syntax is unfamilar, but we will cover
that in the next section.
Like functions, operators are pushed onto the stack in literal form and as such, are identical to regular functions until bound to an identifier.
Lists¶
The primary datatype in xs is the list datatype, represented internal by an OCaml array. Creating a list is easy:
xs> [1 2 3]
0: [1 2 3]
Even though this looks like a list literal, both [
and ]
are
actually functions defined in the language. ]
records the current
length of the data stack and pushes onto a special stack only used for
list construction. [
then compares the length of the stack now to
the value pushed upon the list construction stack and creates a list
with a length equal to the difference between the two, using the
elements from the top of the stack to populate it.
Below are some operations we can do on lists:
xs> ([1 2 3]),[4 5] ! concatenate two lists
0: [1 2 3 4 5]
xs> ([0 3])@til 10 ! select the 0th and 3rd elements
0: [0 2
xs> rev til 3 ! reverse
0: [2 1 0]
xs> flip [[1 2][3 4][5 6]] ! flip rectangular list
0: [[1 3 5] [2 4 6]]
xs> 3?[1 3 5] ! return index of element
0: 1
xs> 3 enlist 1 2 3 ! make a list
0: [1 2 3]
xs> ^[1 2 3] ! push the list elements onto the stack
2: 3
1: 2
0: 1
xs> 4#0 ! create a list of length 4 containing all 0s
0: [0 0 0 0]
xs> dsc til 5 ! sort descending
0: [4 3 2 1 0]
xs> 2#til 10 ! take first 2 elements
0: [0 1]
xs> (neg 2)#til 10 ! take last 2 elements
0: [8 9]
xs> 2_til 5 ! drop first 2 elements
0: [2 3 4]
xs> (neg 2)_til 5 ! drop last 2 elements
0: [0 1 2]
Most functions and operators in xs operate on lists as well as atoms:
xs> 1+[1 2 3]
0: [2 3 4]
If we wanted to have the list be the first argument instead of the
integer 1
, we would need to surround it in a function:
xs> ([1 2 3])+1
0: [2 3 4]
This is because a list is only created after the [
function is
executed, the +
operator would not see the list as its first
argument, it would only see the 3
. To solve this problem, A
function is passed instead, which is evaluated by +
and result
added to 1
. For this reason, all operators in xs can take a
function as their first argument.
To generate a list from 0 to 4 (exclusive), we can use the til
function:
xs> til 5
0: [0 1 2 3 4]
If instead we wanted a list from 5 to 10, we can simply add 5 to it:
xs>5+til 5
0: [5 6 7 8 9]
Now what if we wanted the squares between 5 and 10?:
xs>(x*x~)'5+til 5
0: [25 36 49 64 81]
This introduces us to a new function, the '
or map function. '
simply applies the given function to each element of the list and
pushes the new list onto the stack. Now let’s multiply them all
together:
xs>*/(x*x~)'5+til 5
0: 228614400
The /
or fold function successively applies the given function to
the list until a single result is produced. For more information about
folds, click here.
In order to see the intermediate results, we can use the \
or scan
function:
xs> *\(x*x~)'5+til 5
0: [25 900 44100 2822400 228614400]
Note that scan uses the first element of the list as the first element of the output list.
Scoping¶
xs uses dynamic scoping which means that variables in a function refer to the context in which they are called, not in the one that they are written in:
xs> f:(x)
xs> (x:5; f).
0: 5
While dynamic scoping is more flexible and powerful than lexical scoping, care must be taken to not accidentally refer to an incorrect variable in the surrounding scope.
Data Types¶
Below is a table of data types in xs:
symbol |
description |
example |
---|---|---|
`Z |
61-bit integer type |
42 |
`R |
64-bit floating point type |
1.235 |
`B |
Boolean type |
1b |
`Q |
Symbol type |
`hello |
`S |
String type |
“world” |
`F |
Function type |
(3*2+) |
`L |
List type |
[1 2 3] |
`H |
I/O handle |
h:open `R “foo.txt” |
`N |
Null type |
0n |
In order to convert from one data type to another, we can use the
of
operator:
xs> `Z of 3.2
0: 3
xs> `s of 42
0: "42"
Day 1 Advent of Code 2019 Solution¶
To conclude this quick tutorial, we’re going to solve both parts of the Day 1 Advent of Code 2019 problem. Read the part 1 problem first and then come back here.
First we need to read in the file of masses in order to calculate the required fuel:
masses:(`Z of)'readl "ex/1.txt";
The readl
function reads the given file and returns a list of each
line of the file. In order to convert the strings to integers, we
first use the '
(map) function to apply the conversion to each
element of the list. The of operator converts one datatype to
another: the `Z
quoted symbol tells the operator to try and
convert to an integer. After the conversion, we assign the entire list
to the variable masses
.
Now that the data is read in and in the proper form, we need to write the function to calculate the fuel usage:
fuel:(x:;(x%3)-2);
This function implements the requirement specified in the problem
“Fuel required to launch a given module is based on its
mass. Specifically, to find the fuel required for a module, take its
mass, divide by three, round down, and subtract 2.” Note that %
is
used for division instead of the traditional forward slash. Now let’s
calculate the fuel required and print it to the screen:
0n:sum fuel masses;
Since each operation in the fuel
definition applies to lists, we
don’t need to use the map operator. The sum
function simply adds
up all the numbers in a list and is equivalent to +/
. Next we
assign the result to null, represented by 0n
in xs. This has the
effect of printing the result to the screen.
The Advent of Code site doesn’t display the part 2 problem until part 1 has been submitted, so, if you haven’t already, create an account and execute the xs code on your given input in order to view part 2. Come back here once you’ve read it.
Now that part 1 is out of the way, here’s part 2:
0n:sum(sum(where 0<x)@x~fuel fixes)'fl;
What a doozy! Let’s piece it apart, identifier by identifier. Part two
calls for recursively calculating the weight of the fuel, so like
before we’ll need to apply some function to the fuel masses and then
add them all up. We first apply the fixes
function, which is
similar to \
(scan), except that it continues applying the given
function until one of two conditions is met:
Two consecutive results are equal
The output equals the initial input
fuel
is passed as the input to fixes
and is applied until two
consecutive outputs are the same, which means that we don’t need to
calculate for anymore fuel. But because the fuel amount could be
negative, we need to remove the negative numbers from the list. The
@
operator takes either a number of a list and returns those
elements of a list. For example:
xs> ([0 2])@[1 3 2]
0: [1 2]
The where
function returns the indices of the list if given a list
of binary values:
xs> where [1b 0b 1b]
0: [0 2]
If given a list of integer values, it works like this:
xs> where [1 2 3 1]
0: [0 1 1 2 2 2 3]
So we use the 0<x
expression to return a binary list, indicating
where the positive numbers are; then, we use where
to return the
indices of the list; and finally, we use the @
operator to select
out those indices. Now that we have our list, we simply sum
it in
order to get the total fuel requirement for that module. We sum
to calculate the total fuel requirement for all modules.
Below is the script in full:
masses:(`Z of)'readl "ex/1.txt"; ! read in data
fuel:(x:;(neg 2)+x%3); ! fuel function, broadcasts implicitly
fl:fuel masses; ! calculate masses for each module
0n:sum fl; ! sum masses for part 1
0n:sum(sum(where 0<x)@x~fuel fixes)'fl; ! recursively calculate and sum
In order to run the script in full, save the above in a file (xs
uses the .xs
extension), and run with:
$ xs /path/to/file
Now that you’re finished reading this tutorial, you’re ready to check out the API documentation for a full reference of all xs functions and operators! xs is a unique language and might take a little bit getting used to. If you have any questions, don’t hesitate to open an issue on github at the project page or shoot me an email at sturm@cryptm.org