Getting to Know ParselTongue
This document is a tutorial and language guide for learning ParselTongue. You will get the most out of it if you stop to run the examples, read the questions, and tweak the examples to see what happens. Every time you do, you've created a new test. Keep in mind also that while your immediate goal is to test ParselTongue, you'll be implementing the whole language within a month, so details and corner cases you learn now (and test for!) will only help you later.
In the tutorial, we assume that some script called psl will run ParselTongue programs given as file arguments. Here's a simple bash script that you can use; copy-paste and store in a file named psl in the directory you unpack the binary to:
#!/bin/bash
./debian-dist/bin/assignment1-debian --interp < $1
Note that the above is just for brevity of examples; you are free to run
$ ./debian-dist/bin/assignment1-debian --interp < filename
each time if you like (or the equivalent command for your architecture).
ParselTongue by Example
Here are a few examples of classic programs in ParselTongue, to get you acquainted:
fibs.psl
# deffun defines a recursive function
deffun fib(n)
# operators like + and == are prefix
if <(n, 1) then 0 else
if ==(n, 1) then 1 else
+(fib(-(n, 1)), fib(-(n, 2)))
# The in ends the function body in a deffun, which can be followed by
# another deffun, a defvar, or a braced expression
in
# defvar creates a new variable with an initial value, bound inside the
# body of the defvar
defvar x = 0 in {
# For loops have an initialization, a test, an update, and a body
for(x = 0; <(x,10); x++) {
# print takes any value and displays it
print(fib(x));
print(" ");
};
# The result of the program is the value of the last expression
# evaluated. It is printed, so we avoid printing it by terminating the
# program with a ""
"";
}
$ psl fibs.psl
0 1 1 2 3 5 8 13 21 34
list.psl
# When empty is called, a new object with the isempty field mapping to a
# function is created
deffun empty() { isempty: lambda(self) { true } } in
# Here we define a cons, which allows accessing its first element and the
# rest of its elements. It reports that it is not empty
deffun cons(elt, lst) {
first: lambda(self) { elt },
rest: lambda(self) { lst },
isempty: lambda(self) { false }
} in
# length() takes a list and computes the number of elements in the list.
# The @ syntax is the syntax for message passing
deffun length(lst)
if (lst@isempty()) then 0
else +(1, length(lst@rest()))
in {
print(length(cons(5, cons(2, empty()))));
print(" ");
print(length(empty()));
"";
}
$ psl list.psl
2 0
The Basics
We'll give a more in-depth tutorial by examining features one at a time. The smallest ParselTongue programs are just values; we'll start there.
5.psl
5
$ psl 5.psl
5
The example's formatting means that the file 5.psl
contains just
the character 5
, and when we evaluate it with ParselTongue, we get 5
back.
Let's try it again:
str.psl
"a string"
$ psl str.psl
a string
Here, we printed out the string (with no quotes). That's how ParselTongue prints strings.
We can also evaluate booleans:
true.psl
true
$ psl true.psl
true
ParselTongue is, of course, Object Oriented. Objects are defined with curly braces:
object.psl
{x:5}
$ psl object.psl
object
ParselTongue doesn't print all the fields of an object; it just prints the string "object" when the program's answer is any object.
We can get fields out of objects, using the dot operator. For now, we'll just get fields directly:
getfield.psl
({x: "my field value"}).x
$ psl getfield.psl
my field value
(The parentheses are sometimes necessary in ParselTongue for annoying
parsing reasons. ParselTongue version 0.2 will have better surface
syntax.) We can see that by accessing the x
field, we get back the
string value "my field value"
that we stored in the field.
We can also use the bracket operator to get fields via a string value:
getfieldstr.psl
({x: "my field value"})["x"]
$ psl getfieldstr.psl
my field value
What happens if we try to get a field that isn't there? Let's try it:
getnofield.psl
({x: "my field value"}).y
$ psl getnofield.psl
Field not found: y
ParselTongue tells us that the y
field wasn't found, and prints an
error message telling us so.
We can change the fields on objects after we create them, by using dot and equals:
addafield.psl
({x:"before"}).x = "after"
$ psl addafield.psl
object
Well, the result is an object... But we need to see something about the object to tell if our program worked. Let's try something a bit more sophisticated:
addafield2.psl
(({x:"before"}).x = "after").x
$ psl addafield2.psl
after
This requires a little unpacking. The first thing that happens here is the
definition of the object {x:"before"}
. Next, its x
field is changed to
"after"
. Then, its x
field is accessed (the outermost .x
). Now we see
that the result of the dot-and-equals operation updated the x
field.
What happens if we add a field that doesn't exist? Try it!
Printing things, and doing more than one thing
Sometimes, it's nice to see intermediate output from programs, rather than just
the final answer. It would be nice to be able to see several different
outputs, as well. In ParselTongue, we can chain sequences of statements by
using semicolons, and print things using print
:
print-sequence.psl
print("hello!\n");
print({x:2});
print("\n");
"end";
$ psl print-sequence.psl
hello!
object
end
Here, three print
expressions evaluated one after another, and finally the
program resulted in the string "end". ParselTongue printed the first three
strings (one was interpreted as a newline by the shell), and then printed the
answer of the program, "end". When print
evaluates, its result is the string
that it printed:
print-value.psl
print("hi!")
$ psl print-value.psl
hi!hi!
The string "hi!"
prints twice, once for the effect of print
, and once
because the program's answer is "hi!"
.
Primitive Operations on Values
As a Web language, ParselTongue has basic support for arithmetic, string
manipulation, and value comparison. The operators in ParselTongue are <, >,
+, -,
and ==
. This sample program shows some of their uses. Note that all
operators are prefixed in front of their arguments, which are enclosed in
parentheses and separated by commas:
operators.psl
print(<(3,4));
print("\n");
print(>(4,5));
print("\n");
print(+(3,4,5));
print("\n");
print(-(9,8,7));
print("\n");
print(+("one ", "two ", "three"));
print("\n");
print(==(5,5));
print("\n");
==("foo",5);
$ psl operators.psl
true
false
12
-6
one two three
true
false
What happens if you apply these operators to mixed types of arguments, including objects? Try it!
Note that now we can compute the names of fields that we'd like to look up:
objfieldplus.psl
({longfield: 5})[+("long", "field")]
$ psl objfieldplus.psl
5
Naming and changing things
In our examples above, we could only use each value we made once. ParselTongue lets us save our values to use later by using defvar
blocks:
defvar.psl
defvar o = {x: "myfield"} in { o.x }
$ psl defvar.psl
myfield
This code defines a variable o
, which is bound to the object value {x:
"myfield"}
. The curly braces around o.x
delimit the definition's body.
Inside the braces, we can use o
as a way to refer to the object we defined
earlier, so we're doing an object field lookup just like before. We can chain
defvar
statements together for multiple definitions, with one final body.
They can be used to bind any value, or even evaluate an expression first and
store the result, as in this example:
defvars.psl
defvar o = {x: "myfield"} in
defvar f = o.x in { f }
$ psl defvars.psl
myfield
Here, we used two defvars
, one after another, to first define the object that
is referred to by o
, and then to store the x
field of o
in the variable
f
. This body of the second defvar
is the result of the whole program,
which we see in the output.
Once we've defined a variable, it doesn't have to stay the same forever; ParselTongue is an Imperative language, after all. We can use the assignment operator, =, with variables to change their value:
changevar.psl
defvar x = "before" in {
x = "after";
x;
}
$ psl changevar.psl
after
The variable x
starts with the value "before"
, but changes to "after"
because of the assignment. We can check with a print
statement to be sure:
changevar2.psl
defvar x = "before" in {
print(x);
print("\n");
x = "after";
x;
}
$ psl changevar2.psl
before
after
We can see for sure now: x
was "before"
until the assignment, and then its
value changed.
Earlier, we saw that we could also use =
to change fields on objects, and
here we saw that changes to variables made with =
are visible later. Is
the same true for objects? Try it!
We can combine assignment with the +
and -
operators we saw earlier. By
using +=
and -=
, we can write a shortcut for performing the operation and
an assignment:
changecombo.psl
defvar x = 24 in {
x += 18;
x;
}
$ psl changecombo.psl
42
The variable x
started at 24
, and its initial value was added to 18
and
reassigned into it by the second line. Compound assignment works on objects,
too, but has a slightly different result; it returns the entire object with the
operation performed:
objectcombo.psl
defvar o = {x:"four"} in {
(o.x += "score").x
}
$ psl objectcombo.psl
fourscore
Object assignment works this way because the assignment makes a new copy of the object, rather than overwriting the old field. For example:
objectcombo2.psl
defvar o1 = {x:"four"} in
defvar o2 = o1.x += "score" in {
print(o1.x);
print(" ");
o2.x;
}
$ psl objectcombo2.psl
four fourscore
This shows an important feature of objects in ParselTongue: they are merely flat data structures, and adding and removing fields copies them and updates them, rather than making changes for all holders of the object to see.
Finally, as a Systems programming language, ParselTongue supports the increment
and decrement operators. The operators ++
and --
can appear before or
after a variable, and will either add or remove 1 to it. If they appear
before, the result of the expression is the modified value; if after, it is the
original value. Concretely:
inc-dec.psl
defvar x = 5 in {
print(x++);
print(" ");
print(++x);
print(" ");
print(x--);
print(" ");
(--x);
}
$ psl inc-dec.psl
5 7 7 5
Making decisions and doing things more than once
Most interesting programs make decisions. ParselTongue supports this through
if-then-else
expressions. These expressions switch which branch they execute
depending on if the test is false
or not:
if.psl
if false then print("then branch\n") else print("else branch\n");
if true then print("then branch\n") else print("else branch\n");
$ psl if.psl
else branch
then branch
then branch
What happens if you give something other than false
and true
to if
? Try
it!
It's useful for programs to repeat the same operation multiple times. As a
Procedural programming language, ParselTongue provides two mechanisms for doing
so. The first is the while
loop, which will repeatedly perform an operation
until a test yields false
:
while.psl
defvar x = 10 in {
while(>(x,0)) {
print(x);
print(" ");
x = -(x,1);
}
}
$ psl while.psl
10 9 8 7 6 5 4 3 2 1 0
The body of the loop executed until the comparison >(x,0)
failed (when x
became 0). What was the result of the program? What happens if an error
happens in the middle of the loop? Try it!
While loops are useful, but often end up repeating the same patterns. The
for
loop gives a convenient form for a number of common loops:
for.psl
defvar x = 0 in {
for(x = 0; <(x, 10); x = +(x, 1)) {
print(x);
print(", ");
}
}
$ psl for.psl
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ,
The for loop executes the first of the three statements in parentheses first.
Then, it tests the middle expression, runs the body, and then finally the third
expression in parentheses. As long as the middle expression keeps evaluating
to true
, it repeats this pattern. The result of the whole loop is the last
expression that it evaluated; does this tell you why there is a trailing comma
in the output? What would the result be if the test had been false
to begin
with? What about for while
loops? Try it!
Functions and methods
Perhaps the most exciting facet of Parseltongue is its Functional capabilities.
ParselTongue uses the keyword lambda
to define functions. Functions do
several important things. We'll start by seeing how to define them:
func1.psl
defvar f = lambda() { print("not til later\n") } in {
print("now\n");
f();
}
$ psl func1.psl
now
not til later
not til later
Here, we defined a function with lambda
, and gave it a print
expression for
a body (the part in between curly braces). We stored it in f
, and when we
wanted to evaluate the expression we had stored in its body, we called it
with f()
. Then the print
expression evaluated and we saw not till later
.
Functions can also abstract over common computations. Much like defvar
,
lambda
s can declare variables that they will use in their body. The values
that get stored in those variables are provided when the lambda
is called:
func2.psl
defvar f = lambda(x, y) { +("Chapter ", x, ": ", y, "\n") } in {
print(f("One", "Object-Oriented Programming"));
print(f("Two", "Functional Programming"));
"";
}
$ psl func2.psl
Chapter One: Object-Oriented Programming
Chapter Two: Functional Programming
Here, the values for x
and y
were determined twice, once for each set of
arguments. Each time, the body of the lambda
stored in f
was evaluated,
and printed the concatenation of the static strings with the provided ones.
Finally, functions can capture variables that are not their arguments, and observe and change their state. This can be used to communicate between two functions, and facilitate a number of interesting patterns:
func3.psl
defvar x = "" in
defvar f = lambda(y) { x += +("f says: ", y, "\n"); } in
defvar g = lambda(y) { x += +("g says: ", y, "\n"); } in {
f("hello!");
g("did you add that?");
f("who let you add to this string?");
x;
}
$ psl func3.psl
f says: hello!
g says: did you add that?
f says: who let you add to this string?
The same variable, x
, is closed over by both f
and g
. Thus, when
either makes a change, it is apparent when the other goes to change the
variable again, resulting in shared state.
What do you think happens if there's a defvar
inside a function that binds
one of the function's arguments again? What if you pass too many or too few
arguments to a function? What if you pass expressions to a function instead of
just values? Try it!
Functions can also be recursive, meaning that they can call themselves. To
create a recursive function (or to create a named function with easy
shorthand), use the deffun
construct:
deffun.psl
deffun times(n, m)
if <(n, 1) then 0 else +(times(-(n,1),m),m)
in {
times(3,4)
}
$ psl deffun.psl
12
Here, times
can call itself during its own execution, enabling a loop purely
within a function. What would happen if you tried to emulate this with
defvar
?
ParselTongue has support for message passing as well. Methods in ParselTongue
are merely lambda
s that happen to be stored as fields on objects. The
special @
form allows passing an object as the first parameter of the
lambda
. For example, we would call translate
a method here:
methods.psl
defvar point = {
x: 2, y: 3,
translate: lambda(self, x, y) {
(self.x += x).y += y
}
} in
defvar point2 = point@translate(4, 5) in {
print(point2.x);
print(", ");
print(point2.y);
"";
}
$ psl methods.psl
6, 8
Here, point
is initially set to an object with x
and y
fields, each set
to a number. It also has a field called translate
, which is a function of
three arguments. The variable point2
is then set to the result of
point@translate(4,5)
. The translate
function produces a new object with
both the x
and y
fields changed by the amount passed in. The @
operator
made it so the self
parameter was bound to the initial point
.
What happens if the parameter isn't called self
? What if the program used a
dot before translate instead of an @
?
Note that as with field access, we can also use obj@[expr]()
to compute a
string field name to access for message passing.