Quirl Tutorial

This tutorial teaches basics of the Quirl programming language. The intended audience is people with an interest in Quirl, of course. It's helpful, if you already have some programming experience and kinda like math. Try to solve some of the challenges in the Quirl chat yourself.

Comments and Commas

A comment in Quirl is an informal note for you, the human reader. It starts with # and continues to the next hard line break. Comments may not be the most exciting feature, but we talk about them first so that they won't be mistaken for something else later.

# To the Quirl interpreter, a comment is just empty space.
# In space no one can hear you scream.
# AAAAAHH!!! :)

A comment does not have to begin at the start of a line. You'll often find it after code on the same line. Besides comments and actual whitespace, one other thing is treated as space in Quirl: the comma. So 7,9 is the same as 7 9 to Quirl: two integers separated by space.

Truth Values

The truth values true and false are represented by T and F in Quirl.

T # true
F # false

Exact Numbers

Quirl is well suited for calculations. It converts automatically between different types of numbers as needed. Numbers are written in our familiar decimal system. Quirl supports arbitrarily large integers: the limit is what your system can manage. Here are some examples:

0
-1
1729
+007 # same as 7
808017424794512875886459904961710757005754368000000000

Rational numbers are represented by a fraction of two integers, the latter unsigned. The slash / serves as fraction bar in between. But you can also write rationals as a decimal fraction with a dot as decimal separator. Repeating decimals are supported: mark the start of the repetend with an apostrophe '.

1/3
-3/4
-0.75 # same as -3/4
3.'142857 # same as 22/7
467807924713440738696537864469/935615849440640907310521750000

Rationals are algebraic numbers of degree one. Quirl also handles algebraic numbers of degree two precisely. These include the square roots of square-free integers, the golden ratio and the imaginary unit i. The backslash \ before an unsigned integer substitutes for the radical symbol √ in Quirl. Valid numbers:

\2 # square root of 2
9i # nine on the imaginary axis
-4/3\6i # minus four thirds of the square root of six on the imaginary axis
1/2+1/2\5 # the golden ratio
2221564096/491993569+283748/491993569\462 # Freiman's constant

Finally, Quirl can deal with sums of such numbers exactly. So you can add or subtract multiple integer or rational terms, each one optionally followed by the square root of an integer, optionally followed by the imaginary unit i, without loss of precision.

1+1 # same as 2
1/4\2+1/4\6-1/4\2i+1/4\6i # a primitive 24th root of unity

Challenge

It's your turn! Represent the number one half in different ways that Quirl understands. How many can you think of?

Solution

Here is a bunch of number representations that Quirl recognizes as one half:

1/2, 0.5, 7/14, 0.4'9, 1/6\9, -1/10+3/5

Yes, 0.4999… with 9s repeating without end is exactly one half, too! Don't worry, if your solution is not among those shown here. There are infinitely many ways to write one half in and outside of Quirl. Post yours in the Quirl chat and check whether the interpreter replies with 1/2 in each case.

Floating-point Numbers

Not all numbers can be saved precisely. For instance, the circle constant π, Euler's number e or the cube root of 2 can only be approximated in Quirl. We do that with a floating-point number: a finite decimal that may be multiplied or divided by an unsigned power of ten. It has an exclamation mark at its start.

!0                  # approximately zero
!1.2599210498948732 # approximately the real cube root of 2
!9.1093837139/10^31 # approximate electron mass in kg
!5.972168*10^24     # approximate mass of the earth in kg

Floating-point numbers can be complex in Quirl. That is a sum of real and imaginary part, the latter followed by the imaginary unit i. As usual, neither a zero part nor a factor of 1 before i need to be written.

!i # approximately the imaginary unit
!0.5+14.13472514173469379i # approximately a zero of the Riemann zeta function
!-0.6299605249474366+1.0911236359717214i # approximately a third root of 2
!-0.6299605249474366-1.0911236359717214i # approximately another third root of 2

Challenge

Try to represent the numbers one half and one third as floating-point numbers in Quirl.

Solution

Here are a few ways to represent one half as floating point number in Quirl:

!0.5, !50/10^2, !0.0005*10^3, !0.5+0i

You can check your own solutions in the Quirl chat. Post them and see whether the interpreter replies with !0.5.

What about one third? Unfortunately it can't be precisely represented as floating-point number. This isn't Quirl's fault: it's due to how floating point numbers are encoded in binary – or decimal for that matter – in general. Hence the exclamation mark as imprecision warning in Quirl. So here is a floating-point approximation to one third:

!0.3333333333333333

Statements

All your messages in a Quirl chat make up a program. A program is divided into statements. You automatically end a statement by submitting a message. But you can also divide a single message into statements with a semicolon.

# here are exactly two statements:
1, 2; 3

It's a common practice to place the semicolon at the end of a line and start the next statement on a new line. Each statement is processed as a whole. So an error somewhere in a statement causes it to fail, but the next statement could be processed successfully again.

Errors

The Quirl interpreter will occasionally respond with an error message. Expect three kinds of errors:

# Typo?
Syntactical mistakes in your code that are identified in the early parsing stage.
# Exception:
Errors encountered by the interpreter while processing your code. You can trigger your own exceptions, too, but this is hardly useful except for testing purposes.
# Sorry!
JavaScript errors. That is mostly errors in the Quirl interpreter itself. These should usually not happen. Sometimes it's your fault though. This is not to blame you, but to give hope that you may be able to fix it.

Your browser may also alert you about the page not responding. The Quirl interpreter won't reject a complicated task; it tries to do the computation no matter how long it takes. It's up to you to make your browser stop it on a futile mission.

Challenge

Submit the following erroneous code in the Quirl chat to provoke the three kinds of error messages.

4/0;
inv(0);
ouroboros(?) = ouroboros($a);
ouroboros(1)

Don't worry, if you don't understand what's happening there. We haven't discussed functions like inv, the wildcard ? or placeholders like $a yet, but we will explain one after the other. The take-away message here is that errors happen. It's part of the programming experience and no reason to panic.

Text

Text is wrapped in quotation marks. You can have quotation marks in text by doubling them up. Here are three texts:

"Hello world!",
"A text can contain the symbol # and
space and span multiple lines;
it can also contain semicolons.",
"And I said, ""What about Breakfast at Tiffany's?"""

Constants

You can define a constant by assigning a value to a name. A name can contain letters and low lines, also known as underscore characters. Use the equals sign = as definition operator between name and value. Here are a few examples:

PHI = 1/2+1/2\5;
TAU = !6.2831853071795864769;
TEST_TEXT = "The quick brown fox jumps over the lazy dog."

Defining a constant is also called aliasing. The definition of a constant is a statement. It must be separated from other statements by a semicolon. Otherwise we would not know where the definition starts or ends.

Challenge

You can use A, B, C, D or E as names for constants. You can't assign a value to F though. Why is that?

Solution

F is already defined by Quirl. It represents the truth value false.

Collections

Quirl understands three kinds of collections of values: lists, tuples and sets. A list is just items separated by space. Mind you, commas and comments count as space as well. Lists are flat: when you insert a list into another list, you'll get one long list of both their items instead of a list nested inside a list.

-1, 0, -1, 2+i, 3/5; # a list of five numbers
Pol Int;             # a list of two types
T "love", 4 "me"     # a list of four items

A tuple is basically a list in square brackets. But tuples are not flat: a tuple can have an element that is itself a tuple. Here is a list of three tuples:

[Rat, add, mul] # a tuple of a type and two functions
["Alfred J. Kwak", 1989] # another tuple
[[5  7  6  5]
 [7 10  8  7]
 [6  8 10  9]
 [5  7  9 10]]  # a tuple of tuples of numbers

The order of items in lists and tuples matters. Also, both lists and tuples can hold multiple identical items. These properties do not apply to sets, however. Sets in Quirl behave like unordered sets in mathematics. They are collections of distinct elements whose order is insignificant.

{} # the empty set has zero elements
{F, T} # the set of truth values has two elements
{2, !2, {2}} # this set has three elements: 2, approximately 2 and a set
{1+1, 18/9, \4} # this set has only one element: the number 2
{"circle", "ellipse", "hyperbola", "parabola"} # a set with four elements

Challenge

Have a look at the following unclean code, in which two lists are defined as constants:

ONES = 1 1;
LIST = [2 3 4], # AAAAAHH!!!
5,, , ONES, ":)";

Can you tell how many items the deliberately confusing LIST has?

Solution

LIST has five items:

  1. the tuple [2 3 4]
  2. the number 5
  3. the number 1
  4. another number 1
  5. the text ":)"

Did you get the correct number? In that case: congratulations!

Functions

Quirl is a functional programming language. So, naturally, functions are really important. The cool thing about programming is that you can write your own functions. But Quirl also comes with predefined functions which you can use and build upon.

To apply a function, write its name immediately followed by a list of arguments in parentheses.

ord(50, 101);       # returns whether 50 is smaller than or equal to 101
mul(50, 101);       # multiplies 50 and 101
sum(range(1, 100)); # range returns all integers from 1 to 100, sum adds them
size({2, !2, {2}}); # returns the number of elements in the set

Defining a Function

A function in Quirl is defined by assigning an expression to a name with a parameter list in parentheses. The equals sign = serves as definition operator. We will start with a very basic example: the function's name is one, its parameter list is empty and the assigned expression is the number 1.

one() = 1;

This first example is not very practical though. We could have defined a constant instead. So let's define something useful: the logical or function. The function accepts two arguments and returns T for true whenever at least on argument is true. Otherwise it returns F for false.

or(F F) = F;
or(F T) = T;
or(T F) = T;
or(T T) = T;

Notice how the function is defined piecewise for different inputs. Piecewise function definitions are highly typical for Quirl. You can add as many mappings for the same function name as you want as long as their parameter lists – or their soon to be discussed conditions – are mutually exclusive.

Challenge

Define the logical and function.

Solution

There are multiple ways to define the and function. One possibility is this:

and(F F) = F;
and(F T) = F;
and(T F) = F;
and(T T) = T;

If you submit this definition in the Quirl chat, you'll get the following response from the interpreter:

and, 1;
and, 2;
and, 3;
and, 4;

The integer after the name says how many mappings are known for this function. This should be 1 for a new function. Another number would indicate that the function already exists. It could be predefined in Quirl. Consider to revert your mapping then and choose a new name to avoid conflicts.

Wildcard and Variadic Parameters

Above we defined a logical function with mappings for each valid combination of two arguments. This works for the limited number of truth values T and F. It wouldn't work to, say, duplicate whatever is passed to a function. A wildcard parameter ? helps in that case: it matches any argument.

duplicate(?) = $a $a;

Most programming languages require you to name parameters. Not so Quirl: placeholders $a, $b, $c etc. refer to a function's first, second, third etc. parameter. So the above mapping makes duplicate return a list that contains whatever argument is passed to it twice. What if you want to duplicate two arguments though?

duplicate(? ?) = $a $b $a $b;

What about three? Well, we can't add a mapping for each imaginable number of arguments. Instead, we define a new function and mark its single parameter with three leading dots as variadic. A variadic parameter can match any number of arguments – zero, one, three, twenty.

duplicate_all(...?) = $a $a;

A variadic parameter is still a single parameter. Hence a single placeholder, in this case $a, refers to the variadic parameter and gets replaced by all arguments it matches when the function is called. A parameter list can only have one variadic parameter, but it can be combined with multiple other parameters.

Challenge

Submit the definition of duplicate_all in the Quirl chat, if you haven't already. Then call the function with different arguments, for example:

duplicate_all("yo");
duplicate_all();
[duplicate_all(2 0)];
duplicate_all(F, i, T)

Recursion

In the exercise above, the Quirl interpreter duplicates F, i, T as F, i, T, F, i, T. Let's have a look at the definition of a different function that duplicates each argument individually so that the response becomes F, F, i, i, T, T instead.

duplicate_each(? ...?) = $a $a duplicate_each($b);
duplicate_each() = ;

Observe that the first mapping has two parameters: a normal wildcard that matches one argument and a variadic wildcard that matches all other arguments. So $a $a on the right side only duplicates the first argument. Then duplicate_each is called with the rest of the arguments. The function calls itself. That's recursion.

The function duplicate_each will eventually be called with an empty argument list recursively, because it is re-called without its first argument each time. The empty call doesn't match the first mapping, which expects at least one argument. Instead, the second mapping without parameters matches and ends the recursion.

Challenge

Program a function etacilpud that duplicates all arguments, but returns the copy in reverse order. So the call

etacilpud("r", "e", "d")

shall be answered with:

"r", "e", "d", "d", "e", "r"

Solution

There are usually different ways to solve a task. Here is one:

etacilpud(? ...?) = $a etacilpud($b) $a;
etacilpud() = ;

Challenge

Program a function palindrome that works like etacilpud above except that the last argument of the input does not get duplicated this time. So the call

palindrome("r", "a", "c", "e")

shall be answered with:

"r", "a", "c", "e", "c", "a", "r"

Solution

Here is a solution:

palindrome(? ...? ?) = $a palindrome($b $c) $a;
palindrome(?) = $a;

Congrats, if you found a working solution yourself! But don't worry, if you didn't. It usually takes time and practice to get familiar with recursion – especially in an entirely unfamiliar language.

Type and Trait Parameters

Often you neither want to define a function for specific values only nor with a wildcard parameter that accepts anything. Instead you can restrict valid arguments to a certain type like Rat for rationals or Text for text. The following mapping defines a function that returns the next integer for any given number of type Int.

next(Int) = add($a 1);

Quirl supports union types: Set+Tuple matches sets and matches tuples. Traits are predefined union types with an own name. They refer to all basic types who share certain characteristics, for example being Ordered or Scalable or having the algebraic properties of a Field.

Quirl also supports intersection types: Ordered*Group refers to types that are both ordered and meet the requirements of an algebraic group. Here is a mapping where that makes sense, because min and max find the minimum and maximum of values that can be ordered while sub for subtraction works on groups:

statistical_range(Ordered*Group ...Ordered*Group) = sub(max($a $b) min($a $b));

Function Composition

You can create composite functions by connecting multiple other functions with the composition operator @. For instance,

sum@rev@split("Hello World!")

returns the same result as if we had written:

sum(rev(split("Hello World!")))

It's a matter of taste which syntax you prefer for the computation above. However, an advantage of a composite function like sum@rev@split over nested calls is that it can be used as argument of higher-order functions such as map. A higher-order function is a function that expects a function as argument. Try this:

map(sum@rev@split, "stressed", "evil", "guns", "doom")

Conditions

Earlier we've discussed piecewise function definitions. The interpreter needs to know which mapping applies to any given call in that case. We've seen this decided by specific parameter values and by parameter count. Parameter types would also work. A fourth and powerful distinction can be made with conditions.

But let's talk about sorting first. sort is a higher-order function that expects a comparison function as first argument and a bunch of further arguments that shall be sorted. ord is Quirl's core comparison function. It returns a truth value that says whether two arguments are in their type's default order.

sort(ord, 40, 3000, 5, 1, 20);
# answer: 1, 5, 20, 40, 3000;

sort(ord, "delta", "charlie", "echo", "alfa", "bravo");
# answer: "alfa", "bravo", "charlie", "delta", "echo";

sort(ord, "40", "3000", "5", "1", "20");
# answer: "1", "20", "3000", "40", "5";

Text is sorted lexicographically like in a dictionary; integers are sorted by their position on the number line. Makes sense, right? But those sortings differ: shorter integers come before longer ones whereas the short texts "echo" and "5" come last. Let's define a new comparison function shortlex which treats text like integers:

shortlex(Text Text) : not@eq(size($a) size($b)) = ord(size($a) size($b));
shortlex(Text Text) :     eq(size($a) size($b)) = ord($a $b);

Both mappings have a condition. Conditions come between parameter list and equals sign and start with a colon. The first mapping defines shortlex for two texts of unequal size as order of their respective sizes. The second mapping defines shortlex for texts of equal size as (lexicographic) default order. Let's use that:

sort(shortlex, "delta", "charlie", "echo", "alfa", "bravo");
# answer: "alfa", "echo", "bravo", "delta", "charlie";

sort(shortlex, "40", "3000", "5", "1", "20");
# answer: "1", "5", "20", "40", "3000";

A mapping can have as many conditions as you like, each starting with a colon. Mind that conditions must return a boolean value, T for true or F for false.

Challenge

Define a function factorial which computes the product of positive integers up to a given integer recursively. For example, factorial(6) shall be answered with 720 because 1 ⋅ 2 ⋅ 3 ⋅ 4 ⋅ 5 ⋅ 6 = 720. Remember, add, sub and mul are for adding, subtracting and multiplying two numbers. Use conditions, if you need.

Hint

A mathematician could define the factorial function for a non-negative integer n like this:

               ⎧ 1,                    if n = 0
factorial(n) = ⎨
               ⎩ n ⋅ factorial(n − 1), if n ≥ 1

Can you translate this notation to Quirl?

By the way, mathematicians define the factorial of 0 to be 1, as you can see here. The product of positive integers up to 0 is actually empty, since there are no such integers. But it is useful to take the identity element of multiplication as value of the empty product. The identity element of multiplication is 1 for integers. That is, multiplying a number by 1 doesn't change it.

Solution

Of course there are multiple possible solutions again. Here is one:

factorial(0) = 1;
factorial(Int):ord(1 $a) = mul($a factorial(sub($a 1)));

Partial Application

Partial application is a technique to create a simple function without an own name from a more general function on the fly. This is done by calling the general function with an argument list that contains specific values, but also wildcards ?. It is best understood by looking at examples …

Given two integers, ord returns whether the second integer is greater or equal to the first. So ord(1 ?) yields a function that returns whether a single argument is greater or equal to 1. Hence we could test whether 3 is positive: ord(1 ?)(3). Now, that's convoluted. But partial application is useful in higher-order functions:

# function that returns exactly the positive integers passed to it
positive_integers(...Int) = find(ord(1 ?) $a);

Here is another example: the add function can not just add numbers, it also concatenates two textual arguments. The higher-order function map applies the function given as first argument to each of its other arguments. So can you guess what the following call does? Confirm your idea in the Quirl chat!

map(add(? "s") "ape" "bee" "cat" "dog" "elk")

Challenge

Define a function decrement which accepts arbitrarily many integers as arguments and returns each of them reduced by one. So the call

decrement(2, 3, 5, 7, 11, 13, 17, 19, 23)

shall be answered with:

1, 2, 4, 6, 10, 12, 16, 18, 22

Solution

Here is a solution that uses map and partial application of sub:

decrement(...Int) = map(sub(? 1) $a);

As usual, other solutions are possible. We could do without partial application by using a polynomial, x-1 in this case, which we haven't talked about yet:

polynomial_decrement(...Int) = map(x-1 $a);

While convenient, none of Quirl's higher-order functions like map are required to achieve a goal, for all of them have been programmed with Quirl itself. Here is a recursive solution that uses only the core function add:

core_decrement(Int ...Int) = add($a -1) core_decrement($b);
core_decrement() = ;

Wow, you've made it through the tutorial. This is awesome! 🥳 There ist still more to explore, but you have learned a lot already. May Quirl be of assistance to you when you need help with computations.