Getting Started with Quail v2.0 - Quail-Language/quail GitHub Wiki

Getting started with Quail (v2.0+)

Chapter 0: Prerequisties

To begin Quail development, you must install Quail interpreter (Download).

Also, a good text editor with Quail syntax support is recommended (e.g. Sublime Text). You can download custom syntax highlighting files from Quail download page.

If you installed Quail via installer (or did everything correctly during manual install) you can go to Command line/Terminal/Shell and type:

quail info

It should give you information about currently installed Quail version. If it does, then everything's okay and we can move forward.

After doing that, you can write a simple "Hello, world" to test your development environment. Here's a snippet:

print("Hello, world!")

Write this, save it in a file (let's call it test.q). Now you can run it:

quail run path_to_your_quail_code_file.q

For example, if I open terminal in the same folder where test.q is located, I can do:

quail run test.q

Run it. As you can see, print(something) puts that something into console.

We will use print(something) construction a lot, so try to remember how that's written.

Chapter I: Numbers, operators and expressions

Computers were designed to calculate something, so let's make our computer do what it's supposed to do.

To calculate something on your own, you should have some numbers and math symbols. E.g. 2 + 4 * 5. This is what's known as an expression.

In programming we also use expressions and, in fact, they are pretty similar to math expressions. We can write expression written above and try to make computer calculate it for us.

Practice 1.1:

Write a program that calculates 2 + 4 * 5 and puts the answer into the console

If you didn't manage to write a functioning program, don't give up - you're just learning. Anyway, there's snippet for 1.1:

print(2 + 4 * 5)

So, what do we see here? 3 numbers and 2 operators, right? As you can see, we can introduce a number just by writing it down. And notice this thing - 4 * 5 is calculated first, as we do it in maths. This is called operator precedence, when one operation is preferred over another.

Are you ready for another practice?

Practice 1.2:

Try to modify program from 1.1 and make it calculate 2 + 4 first.

Hint: programming is a lot like maths

So, there the answer:

print(  (2 + 4) * 5  )

Again, just like in maths, we use () to denote preferred operation.

Also, notice that I put lots of whitespaces between (()) ? Actually, it does not make any difference, it just makes it easier to read. But this is not the way it should be written. In actual projects there's no big whitespaces between braces and the snippet looks like:

print((2 + 4) * 5)

And it still acts the same! Remember - whitespaces do not make big difference.

Practice 1.3:

Write a program that calculates sum of 10 and 45, then multiplies it by 2

Challenge: ... and then divides it by 3

There are the answers:

print((10 + 45) * 2)
print((10 + 45) * 2 / 3)

So, we have another new operation - division and it is denoted by / slash. Actually, there are more than 4 basic arithmetic operations. I've listed them below:

Operator Operation Example
a + b Sum a and b 2 + 4 == 6
a - b Subtract b from a 3 - 5 == -2
a * b Multiply a and b 7 * 6 == 42
a / b Divide a by b 5 / 2 == 2.5
a // b Divide a by b and make result an integer (floor) 5 // 2 == 2
a ^ b a in power of b 6 ^ 2 == 36
a % b Division remainder 5 % 2 == 1
a Negative a -(2 + 3) == -5
a ^ 0.5 Square root of a 25 ^ 0.5 == 5

Pretty simple, isn't it?

Another thing I want to cover in this chapter is negative and non-integer numbers. I think text descriptions here would be redundant, so I'll just write correct and incorrect ways of writing a number in Quail.

Correct:

print(2)
print(-4)
print(0.5)
print(-0.6)
print(3.1415)
print(4000000)

Incorrect:

print(0,4)
print(.4)
print(-.3)
print(4_000_000)

So now, when we have covered everything about numbers, let's proceed to the final practice.

Practice 1.4

Write a program that calculates sqrt of one thrid of the circumference (length of a circle's outline) of a circle with $R = 4 + 5$ (radius)

Tip: The formula is $\sqrt {{2 \pi R} \over {3}}$

Do not delete this program, we'll return to it later

Answer to final practice:

print(((2 * 3.1415 * (4 + 5)) / 3) ^ 0.5)

Chapter II: First meeting with strings

Our output from 1.4 is not very informative. It's only a number without any description. Let's now add a label to this output. And to do that we'll need strings.

String is basically text. Syntactically, it's denoted with double quotes " at the start and at the end. Example:

print("Oh, hello there!") 

But how we can add some description, like Answer is to our number input? You can use + operator to add string to another string. Let's try it!

print("Answer is " + ((2 * 3.1415 * (4 + 5)) / 3) ^ 0.5)

And... This returns an error. Why? That's the reason: we are trying to add string to a number, not string to string. We can convert number to string using type conversion

Type to string conversion

Syntax: string(value)

Returns: A string containing text representation of value

Try to fix our snippet by yourself!

Practice 2.1: Modify the snippet and make it work without errors

Snippet:

print("Answer is " + ((2 * 3.1415 * (4 + 5)) / 3) ^ 0.5)

Do not delete this program, we'll return to it later

The answer is:

print("Answer is " + string(((2 * 3.1415 * (4 + 5)) / 3) ^ 0.5))

We'll come to strings later, but for now this knowledge is enough.

Chapter III: Input and number conversion

Imagine that you should make a program that can calculate $\sqrt {{2 \pi R} \over {3}}$ for any $R$. End user 90% chance knows nothing in programming, so he cannot just take an editor, open your source code and modify the expression. What should you do then? The answer is - make your program capable of getting user input.

At the moment we know only one function - print. It takes some value and puts into console. Now welcome its relative - input!

input Function

Syntax: input() or input(message)

Action: Waits until user puts something in console and presses Enter, then returns inputted value. If message is provided, then prints an input prompt Returns: string

Get ready for another task

Practice 3.1:

Modify answer from 2.1 to take input instead of hard-coded radius.

Do not forget: $R$ was $4 + 5$

The answer is:

print("Answer is " + string(((2 * 3.1415 * input()) / 3) ^ 0.5))

But, does it work? Let's test it. Unfortunately, no. And the reason here is similar to reason why snippet from Chapter II did not work. We are trying to multiply a number by a string. This is not possible, so Quail gives an error. And the solution is actually very similar to 2.1 solution. Just add a type conversion. But this time - from string to number.

Type to number conversion

Syntax: num(value)

Returns: Number representation of a value

Can throw an error when value is not a valid number

So, here's another practice:

Practice 3.2:

Modify the snippet and make it work

print("Answer is " + string(((2 * 3.1415 * input()) / 3) ^ 0.5))

And the answer is:

print("Answer is " + string(((2 * 3.1415 * num(input())) / 3) ^ 0.5))

The final practice:

Practice 3.3:

Modify the 3.2 answer and make it display an input prompt "R="

Do not delete this program, we'll return to it later

Answer to final practice:

print("Answer is " + string(((2 * 3.1415 * num(input("R="))) / 3) ^ 0.5))

Our snippet gets bigger and more complicated overtime, but don't worry - we'll resolve this problem right now.

Chapter IV: Variables and null

It will be great if we could not only calculate, but also store data. And, actually, we can! We can use variables.

Variable is some sort of a cell in memory. It has a name and a value assigned to it. We can assign any variable at any moment using this syntax: variable = value. We can retrieve data from variable just by writing its name. Referencing to unassigned variable returns null.

Variable name can contain only A-Z, a-z letters, underscores, and numbers (only after at least one letter)

Correct variable names: hello, d2, this_is_MY_variable

Incorrect variable names: @hello, 2d, this is MY variable

Example of variable usage:

a = 10 + 5
b = num(input())
print(a * b + 45)

Practice 4.1

Refactor answer to 3.3. You should store input, $\pi$, whole circumference and stringified answer in four separate variables.

Refactoring is the process of restructuring code, while not changing its original functionality. The goal of refactoring is to improve internal code by making many small changes without altering the code's external behavior

Do not delete this program, we'll return to it later

Answer to 4.1:

R = num(input("R="))
pi = 3.1415
C = 2 * pi * R
answer = string((C / 3) ^ 0.5)
print("Answer is " + answer)

Now your program is looking way better and you can clearly tell what part of it does what.

One little thing I need to tell you about - the null. Null is literally nothing. It's not even $0$. You cannot anything with null, if you'll try - it'll give an error.

Chapter V: Booleans and logic operations

Let me introduce new data type - boolean. Boolean is a very simple type, it can represent only either false or true value. In Quail, you can write false or true to create a boolean. There are also available opertions for booleans and type converter.

Type to boolean conversion

Syntax: bool(value)

Returns: New boolean value that matches the value

                If value is string then acceptable values are "true" or "false"

                If value is number then any value apart from 0 will be true, 0 will be false

Can throw an error if value is not a boolean.

Boolean operations:

Operator Operation Example
a and b
a && b
Logic 'and' true and false == false
true and true == true
false and false == false
a or b
a `|
` b Logic 'or'
! a Logic inversion !true == false
!false == true

Also or is a bit special. If a and b operands are booleans, then it acts as logic 'or', but in other case, table below is applied:

a b a or b
Not null Not null Returns a
Null Not null Returns b
Not null Null Returns a
Null Null Returns null

There are also available comparison operators:

Operator Operation Example
a > b a is greater than b? 4 > 2 == true
a < b a is less than b? 4 < 2 == false
a >= b a is greater than or equal to b? 3 >= 3 == true
a <= b a is less than or equal to b? 5 <= 3 == false
a == b a is equal to b? 6 == 6 == true
a != b a is not equal to b? 5 != 6 == true

Now when you know a lot more about logic and comparison, get ready for practice.

Practice 5.1:

  • Modify answer to 4.1 and make it ask not only radius, but also user's expectations about the value. Then, print message that says was user correct or not.

    Tip: look how $R$ is inputted

  • Also make it calculate just the circumference.

    Tip: delete the answer variable, and convert to string only circumference

Do not delete this program, we'll return to it later

And the answer is:

R = num(input("R="))
expects = num(input("Guess: "))
pi = 3.1415
C = 2 * pi * R
print("Answer is " + string(C))
print("Did expectations met? " + string(expects == answer))

Chapter VI: Branching

At the moment we can input negative radius and that is a problem. But now we can check whether the value is positive (x > 0). And now let me introduce the first construction - if.

if construction

Syntax:

if condition
    action_if_true
else
    action_if_false

Else can be skipped:

if condition
    action_if_true

If condition is true, then code action_if_true will be done. Else if there is else construction, then code action_if_false will be done. If there is no else and condition is not true, then nothing will happen, if will be just skipped.

Notice that space. This can be achieved through pressing TAB key.

With this construction we can check something and make decisions. E.g.:

n = 432552
guess = num(input("Enter your guess"))
if n == guess
    print("You've guessed the number!")
else
    print("Sorry. You are incorrect.")

This snippet provides us possibility to set some number and then make user guess it. If user is correct, then one message will be displayed, else other.

Practice 6.1

  • Now modify the program, so that it will display an error if user's $R &lt; 0$, else it will display $2{\pi}R$

  • Also, remove guessing functionality

Do not delete this program, we'll return to it later

And here comes the answer:

R = num(input("R="))
pi = 3.1415
C = 2 * pi * R
if R < 0
    print("R < 0!")
else
    print("Answer is " + string(C))

Chapter VII: Statements and blocks

Let's say we want to put more than one line into if construction. How we can do that? One could think about this kind of approach:

R = num(input("R="))
if R < 0
    print("R < 0!")
    print("Try again")

But when we run that program, Try again is printed every time, even if R >= 0. That is because of how language sees your code.

There is a basic concept, that's called statement. According to Wikipedia,

statement is a syntactic unit of an imperative programming language that expresses some action to be carried out. A program written in such a language is formed by a sequence of one or more statements. A statement may have internal components (e.g. expressions)

To put it simply, a statement can be equated to the line: one line - one statement.

The if construction looks for one statement after condition and then considers the if section complete. To pack multiple statements to one we can use blocks.

Block is a statement that contains an ordered collection of statements. When block is being executed, each statement in that block will be executed one-by-one from the first to the last. Blocks are usually denoted with curly braces, but in Quail there are other ways to denote blocks:

# The normal way
if condition {
    print("This is statement 1")
    print("This is statement 2")
    print("This is statement 3")
}


# The Lua way
if condition then
    print("This is statement 1")
    print("This is statement 2")
    print("This is statement 3")
end


# The Python way
if condition:
    print("This is statement 1")
    print("This is statement 2")
    print("This is statement 3")


# Btw, you can wrap single or no statements whatsoever:
if condition {}
if condition {
    print("This is the only statement")
}

For simplicity, we will use curly-brace-denoted block throughout this whole tutorial.

Practice 7.1

  • Modify your program to print "Try again!" after printing "R < 0!"

Do not delete this program, we'll return to it later

The answer is:

R = num(input("R="))
pi = 3.1415
C = 2 * pi * R
if R < 0 {
    print("R < 0!")
    print("R < 0!")
} else
    print("Answer is " + string(C))

Chapter VIII: Through loop. While loop

Keep your circumference calculator and start a new program.

Now we want to calculate a factorial of some inputted number.

Factorial of $n$ is a product of all numbers from $1$ to $n$, including $n$. Often written as $n!$

Example: $4! = 1 \cdot 2 \cdot 3 \cdot 4$

How do we do that? Now loops will come in handy.

Loop is a statement that is executed many times, while some condition is met.

The simplest loop is while:

while construction

Syntax:

while condition
    statement

Executes statement repeatedly while condition == true

So, how we should calculate the factorial? We can start from $1$, multiply by this number, increase it by $1$. Continue this while this number is less than or equal to $n$. You can try to write it yourself. Tip: introduce 3 variables: n for inputted number, i for increasing number and r for result

The answer is:

n = num(input())
i = 1
r = 1
while i <= n {
    r = r * i
    i = i + 1
}
print(r)

This will give us the factorial. But is does look quite ugly, doesn't it? We can fix this using through loop:

through construction

Syntax:

through start_number:end_number as iterator
    statement

Behaviour:

Starts at iterator = start_number Executes statement and each time moves iterator by $1$. Continues while iterator < end_number.

Example:

through 1:11 as i {
    print(i)
}

Will output:

1
2
3
4
5
6
7
8
9
10

Note that 11 is not included. To include it, write :+ instead of ::

through 1:+11 as i {
    print(i)
}

Will output:

1
2
3
4
5
6
7
8
9
10
11

Ready for another practice?

Practice 8.1

Rewrite previous factorial program using through instead of while

Tip: do not forget to include $n$ in loop

Do not delete this program, we'll return to it later

The answer is:

n = num(input())
r = 1
through 1:+n as i
    r = r * i
print(r)

Interchapter practice I:

Combine your circumference calculator and factorial calculator into one program

Tip: first ask user for the action (factorial or circumference) then with if and == choose the right code.

Tip 2: you can combine else and if to make conditional else

Do not delete this program, we'll return to it later

The answer is:

action = input("Circumference (C) or Factorial (F)? ")
if action == "C" {
    R = num(input("R="))
    pi = 3.1415
    C = 2 * pi * R
    if R < 0 {
        print("R < 0!")
        print("R < 0!")
    } else
        print("Answer is " + string(C))
} else if action == "F" {
    n = num(input())
    r = 1
    through 1:+n as i
        r = r * i
    print(r)
}

Chapter IX: Functions

Despite all your tries, code again looks quite messy. Let's see another instrument for making code clearer - a function.

Function is a packaged sequence of program instructions that performs a specific task. This function can then be used in programs wherever that particular task should be performed.

function construction

Syntax:

function function_name()
    statement

or

function function_name(function_argument1, function_arg2, ...)
    statement

We can call a function using its name and a pair of round braces:

function_name()

We can also return a value from a function:

function add(a, b) {
    return a + b
}

c = add(5, 9)
print(c)

Let's now split our code using functions. Yes, another practice!

Practice 9.1

Split your code into different functions - one for circumference, other for factorial

Do not delete this program, we'll return to it later

The answer is:

function circumference() {
    R = num(input("R="))
    pi = 3.1415
    C = 2 * pi * R
    if R < 0 {
        print("R < 0!")
        print("R < 0!")
    } else
        print("Answer is " + string(C))
}


function factorial() {
    n = num(input())
    r = 1
    through 1:+n as i
        r = r * i
    print(r)
}


action = input("Circumference (C) or Factorial (F)? ")
if action == "C"
    circumference()
else if action == "F"
    factorial()

The code is much more understandable now!

Chapter X: Lists. Indexing

Let's now say we want to calculate average of some collection of numbers. We can do it easily with 2 numbers:

a = num(input())
b = num(input())
print((a + b) / 2)

We can easily do it with 3 and 4 numbers, but when it comes to big sequences, e.g. 100 or 200 it becomes pretty difficult. And more than that, what if we want to calculate average of sequence of unknown size? List will come in handy.

List is an ordered collection of different elements, with varying size. You can add, remove, insert, access elements and do much more with them.

Defining an empty list l = []

Defining a list: l = [1, 3, 5, 7]

Adding element to list l.add(9) => [1, 3, 5, 7, 9]

Removing element from list l.remove(1) => [3, 5, 7, 9]

Accessing element in list l[0] => 3. Note that in programming we count from zero, so l[0] - first element

Size of list l.size() => 4

So, how do we input a varying-long sequence of numbers?

strings = input().split(" ")
numbers = []
through 0:strings.size() as i {
    numbers.add(num(strings[i]))
}

Let's explain every line:

  1. We input a string and split it using space as separator

    string.split(delimiter)

    Splits string into parts using provided delimiter. Returns list of strings

  2. We create an empty list, where we will put our numbers

  3. Loop through 0 to size of string list, store in i

  4. For each index, get string with corresponding index in strings list, convert it to a number and add it into numbers list

As a result, we get list numbers that contains all numbers, that user has inputted in the following format: 1 3 5 7 9

Practice 10.1

Make a new program that calculates average of all numbers inputted by user. Use code provided above.

Tip: $avg(x_1, x_2,...,x_n) = {1 \over n } \cdot (x_1 + x_2 + ... + x_n)$

Do not delete this program, we'll return to it later

Answer to 10.1:

strings = input().split(" ")
numbers = []
through 0:strings.size() as i {
    numbers.add(num(strings[i]))
}

sum = 0
through 0:numbers.size() as i {
    sum = sum + numbers[i]
}

print(sum / numbers.size())

Chapter XI Appendix I: List manipulation

List operations:

Left Operator Right Operation Example
List + List Concatenate (add) two lists [1, 2] + [3, 4] == [1, 2, 3, 4]
List * Number Multiply list [1, 2] * 2 == [1, 2, 1, 2]
List / Number Divide list [1, 2, 3, 4] / 2 = [[1, 2], [3, 4]]
[1, 2, 3, 4, 5] / 3 = [[1, 2], [3, 4], [5]]
List // Number Divide list into equal parts and throw off remainder [1, 2, 3, 4] // 2 = [[1, 2], [3, 4]]
[1, 2, 3, 4, 5] // 3 = [[1], [2], [3]]
List >> Number Barrel-shift all values to right by certain number [1, 2, 3] >> 1 == [3, 1, 2]
List << Number Barrel-shift all values to left by certain number [1, 2, 3] << 1 == [2, 3, 1]
List <, >, >=, <= List Compare two lists by size [1, 2] > [1] == true
List ==, != List Compare each element in list using == [1, 2] == [1, 2] -> trueList methods:

List methods: See Quail Core Docs on List

Chapter XI: For loop. Range. Sublist

Now, when we introduced lists, time has come for range, sublist and for loop.

Let's start with range: Range is a sequence of numbers from $n$ to $m$ (not including $m$ if denoted with :, included when denoted with :+), where each one differs from its neighbours by $k$. To say more correctly - range is an arithmetic progression. Syntax is: n:m, n:m:k, n:+m, n:+m:k.

Examples:

  • 0:5 = [0, 1, 2, 3, 4]

  • 0:+5 = [0, 1, 2, 3, 4, 5]

  • 3:0 = [3, 2, 1]

  • 0:10:2 = [0, 2, 4, 6, 8]

Range immediatly creates a list, when it is being executed.

Moving forward to sublists: Sublist is a list that has been created from a certain part of other list, starting from index $n$ to $m$ (not including $m$ if denoted with :, included when denoted with :+) with a certain step $k$. Syntax is similar to range.

Examples:

Assume l = ["a", "b", "c", "d", "e", "f"]

  • l[0:2] = ["a", "b"]

  • l[0:+2] = ["a", "b", "c"]

  • l[0:+4:2] = ["a", "c", "e"]

There are a few tricks with sublists:

  • l[:2] = ["a", "b"] l[:n] - from start to $n$

  • l[4:] = ["e", "f"] l[n:] - from $n$ to end

  • l[::-1] = ["f", "e", "d", "c", "b", "a"] Basically, it says: from start to end, every element with step -1 (backwards), so it reverses the list

A little tip: you can use this syntax on strings too

Let's move to for list:

for construction

Syntax:

for iterator in collection
    statement

Behaviour:

Start at first element in collection. iterator is the current element. Walks through all elements in collection, keeping original order.

Example:

for n in 0:5 {
    print(n)
}

Will output:

0
1
2
3
4

Strings are also supported

for character in "abc" {
    print(character)
}

Will output:

a
b
c

Practice 11.1

Refactor 10.1 using for loop

Do not delete this program, we'll return to it later

Answer to 11.1:

strings = input().split(" ")
numbers = []
for s in strings {
    numbers.add(num(s))
}

sum = 0
for n in numbers {
    sum = sum + n
}

print(sum / numbers.size())

Chapter XII: Dictionaries. For loop unpacking

Let's take a break from maths, and do something else. So, what if your calculator needs to be able to display error messages in different languages? The easiest way to do it is a dictionary.

Dictionary is an unordered key-value structure. Each unique key corresponds to certain value. Syntax is: {key=value, key=value, key=value}. You can put items into dictionary, access them, etc. Examples:

Assume d = {"one" = 1, "two" = 2, "three" = 3}

  • d["one"] = 1

  • d["ten"] = null (that key is not present, so it's null)

  • d["ten"] = 10

  • d["ten"] = 10 (now we set "ten" -> 10)

  • d.keys() = ["one", "two", "three", "ten"] (not necessarily in that order)

  • d.values() = [1, 2, 3, 10] (not necessarily in that order)

  • d.pairs() = [["one", 1], ["two", 2], ["three", 3], ["ten", 10]] (not necessarily in that order)

  • d.size() = 4 (number of key=value pairs)

Notice, that dictionaries, unlike lists and strings, do not have strict order, therefore there are no indexes. Dictionaries can only be iterated by keys:

for key in d.keys() {
    print(d[key])
}

Also you should note, that all keys in Quail must be strings.

Let's return to our for loop. It has a secret ability: it can iterate more that one iterator at once. Basically it works like that: let's say we have a loop for a, b in l { ... }. The loop will one-by-one go through elements of l and try to unpack them. If element is a list and its size is equal to iterator count, then each sub-element of that element will be mapped to its iterator. In any other case, there will be an error. Example:

d = {"a" = 1, "b" = 2, "c" = 3}
for key, value in d.pairs() {
    print(key)
    print(value)
    print("---")
}

Will output: (not necessarily in that order)

a
1
---
b
2
---
c
3
---

Let's return to our translator problem, here is the solution:

lang = {
    "en" = {
        "picked_language" = "Language picked",
        "farewell" = "Bye!"
    },
    "de" = {
        "picked_language" = "Sprache ausgewählt",
        "farewell" = "Tschuss!"
    }
}

userLang = input("Choose your language: ")
print(lang[userLang]["picked_language"])
print(lang[userLang]["farewell"])

We are storing dictionaries of different translations under their language keys, then letting user select language, then selecting suitable message dictionary and accessing messages from it.

Practice 12.1

Modify the input line in program above to display all possible languages

Tip: use keys() method

Answer to 12.1:

lang = {
    "en" = {
        "picked_language" = "Language picked",
        "farewell" = "Bye!"
    },
    "de" = {
        "picked_language" = "Sprache ausgewählt",
        "farewell" = "Tschuss!"
    }
}

print("Available languages:")
for language in lang.keys() {
    print(language)
}
userLang = input("Choose your language: ")
print(lang[userLang]["picked_language"])
print(lang[userLang]["farewell"])

Chapter XII Appendix I: Dict manipulation

Dict operators:

Left Operator Right Operation Example
Dict ==, != Dict Compare each key-valuie pair in dict using == {"a" = 1, "b" = 2} == {"a" = 1, "b" = 2} -> true
{"a" = 1, "b" = 2} == {"a" = 1, "c" = 2} -> false

Dict methods: See Quail Core Docs on Dict

Chapter XIII: Generators. Map, sum function. Anonymous functions

It turns out that this chapter is quite confusing and chaotic, please try to understand it)

Let's return to our code from 11.1. We can simplify it further using list generators. List generator is an expression that is repeatedly evaluated in some loop and in the end returns a list. Basically, it's a for or through loop with list.add in it. Let's take a closer look at some examples:

  • [i through 0:5 as i] -> [0, 1, 2, 3, 4] Each iteration, element i is evaluated (which corresponds to currently iterating number) and added into list.

  • [i through 0:5 as i if i % 2 == 0] -> [0, 2, 4] Same as above, but it contains a condition: if condition is true, element is added, else - skipped.

  • [character for character in "abc"] -> ["a", "b", "c"] Same as through, but this time - it uses for

Now we can transform this:

strings = input().split(" ")
numbers = []
for s in strings {
    numbers.add(num(s))
}

To this:

numbers = [num(s) for s in input().split(" ")]

Dictionary generators also are available:

Assume l = "abc"

  • {l[i] = i for i in 0:l.size()} -> {"a" = 0, "b" = 1, "c" = 2}

Nonetheless, we can simplify this number convertor further, using map function:

map(func action, list collection) function

Behaviour: applies function action for each element in collection.

Example:

function f(x) return x^2 map(f, [1, 2, 3]) Returns [f(1), f(2), f(3)] = [1, 4, 9]

So, we can just write this:

numbers = map((x) -> return num(x), input().split(" "))

But, what's that (x) -> return num(x) thing out there? This is called anonymous function. It's like a regular function, but it doesn't have a name, so it's not polluting your namespace. Anonymous functions (also called lambdas) are useful when you need to pass some action as a parameter. The syntax for lambda is: (arg1, arg2, ...) -> statement or function (arg1, arg2, ...) statement.

You can place lambdas in variables to achieve same thing you could do with regular function definitions. E.g. these two snippets are doing the same thing:

function sqrt(x) {
    if x < 0 return -1
    return x^0.5
}
sqrt = (x) -> {
    if x < 0 return -1
    return x^0.5
}

All these things are essentials for the so-called functional programming, which is very effective when you are dealing with data handling. Let's learn another function for this paradigm - the sum function:

sum(list collection) function

Returns sum of all numbers in given collection

Example:

sum([1, 2, 3]) = 6

Let's now modify our average value calculator using all tools we've learned:

numbers = map((x) -> return num(x), input().split(" "))
print(sum(numbers) / numbers.size())

As you can see, it's much more clear and smaller than than our imperative implementation:

strings = input().split(" ")
numbers = []
for s in strings {
    numbers.add(num(s))
}

sum = 0
for n in numbers {
    sum = sum + n
}

print(sum / numbers.size())

You can clearly see the advantage of functional programming when handling data.

P.S.: If you've understood this chapter - congrats, it's one of the most confusing and difficult chapters here (in my opinion)

Interchapter practice II

Uh oh, that's a heavy task.

  1. Combine factorial calculator, circumference calculator and average calculator

  2. Extend average calculator - it should also display min and max number

  3. Implement different parts of calculator with functions

Tip: to calculate min and max, you can use min and max functions. They work like sum. More info about this in Quail Core Docs on min, max

Do not delete this program, we'll return to it later

The answer is:

function circumference() {
    R = num(input("R="))
    pi = 3.1415
    C = 2 * pi * R
    if R < 0
        print("R < 0!")
    else
        print("C=" + string(C))
}


function factorial() {
    n = num(input())
    r = 1
    through 1:+n as i
        r = r * i
    print(string(n) + "!=" + string(r))
}


function averageMinMax() {
    numbers = map((x) -> return num(x), input().split(" "))
    print("Average" + string(sum(numbers) / numbers.size()))
}

print("Available actions:")
print("Circumference    C")
print("Factorial        F")
print("Avg,min,max      A")

action = input("Action? ")
if action == "C"
    circumference()
else if action == "F"
    factorial()
else if action == "A"
    averageMinMax()

Chapter XIV: Introduction to OOP. Classes, objects

Okay, let's now move to some theory. We are going to talk about object-oriented programming. Let's define some concepts:

Object is a packaged collection of variables (fields) and functions (methods). Usually object is a derivative of some class.

Class is a template for creating objects. It defines fields and methods for its objects.

Constructor is a method that created an object from some class (this process is also called instantination or in Quail - derivation).

Let's now get some examples from our world. You can think of an object as a building. It has various parameters, methods to open/close entrance, etc. In this case you can think of a class as it's a blueprint for that building - it defines its parameters and different functions. Constructor is basically the construction team. It creates a new building based on some blueprint you give to them. I hope everything is more clear now and we can proceed on looking at syntax.

Defining a class:

class Building {}

Adding fields:

class Building {
    width = 0
    length = 0
    height = 0
}

Adding methods:

class Building {
    width = 0
    length = 0
    height = 0

    function outerArea(this) {
        return this.width * this.length
    }
}

Adding constructor:

class Building {
    width = 0
    length = 0
    height = 0

    constructor (this, width, length, height) {
        this.width = width
        this.length = length
        this.height = height
    }

    function outerArea(this) {
        return this.width * this.length
    }
}

Instantinating: (create new building with parameters 3, 4, 5 - passed to constructor)

building = Building(3, 4, 5)

Accessing fields:

print(building.width)

Calling methods:

print(building.outerArea())

We need to stop here a little. Note how outerArea, constructor have this wierd first argument called this and note how we pass 1 argument less than needed? (3 args to constructor instead of 4, 0 args to outerArea instead of 1). This magic argument allows methods to access the object that they belong to. When you call some method on an object, Quail will automatically understand that this is an object and it will automatically insert the object as the first argument, moving all arguments to the right. So building.outerArea() will actually be building.outerArea(building). This mechanism allows us to access fields of an object inside its method or call other methods. Please note, that if you save method of an object somewhere else and call it as a function, Quail won't prepend object reference to arguments, so:

f = building.outerArea  # Save building.outerArea method into f
f()                     # Won't work
f(building)             # This is what you need to do

Practice 14.1

Create a Rect class that resembles a rectangle. It should have fields for width, height, and a method for calculating its area.

Then, make user be able to input width and height and see the area of inputted rectangle.

Answer to 14.1:

class Rect {
    w = 0
    h = 0

    constructor (this, w, h) {
        this.w = w
        this.h = h
    }

    function area(this) {
        return this.w * this.h
    }
}


w = num(input("w="))
h = num(input("h="))
rect = Rect(w, h)
print(rect.area())

Chapter XV: Inheritance

Classes can inherit from one another. You can thing of it as creating a new blueprint based on other blueprint. Methods and fields from parent class will remain unless overridden. Inheritance looks like this:

class Letter {
    capital = ""
    small = ""
    type = ""

    constructor (this, type, capital, small) {
        this.type = type
        this.capital = capital
        this.small = small
    }
}

class Vowel like Letter {
    type = "vowel"

    constructor (this, capital, small) {
        Letter._constructor(this, "vowel", capital, small)
    }
}

Here, class Vowel inherits from class Letter, gaining (implicitly) all Letter's fields and overriding the constructor. Note how we call a function with underscore from class? Yes, we can access all methods from class, and even call them, but you will need to manually pass this. E.g. Rect.area(rect) == rect.area(). Constructor is saved as a function with name _constructor. So we are basically calling defined in Letter constructor, but we are substituting type with our predefined "vowel" value.

Another useful thing - instanceof check. You can check whether or not an object is an instance of some class:

Assume: a = Vowel("A", "a"), b = Letter("consonant", "B", "b")

  • a instanceof Vowel -> true - a is an instance of class Vowel

  • b instanceof Letter -> true - b is an instance of class Letter

  • a instanceof Letter -> true - a is an instance of class Vowel which is a child of class Letter

  • b instanceof Vowel -> false - b is not an instance of class Vowel and b's class is not a child of Vowel class

Chapter XVI: Static fields and methods

Methods and fields can be static. This means that they are not bound to some specific object, but they are common for the whole class. You can declare static fields and methods by using static modifier:

class AClass {
    static someStaticField = 23412

    static function f() {
        ...
    }
}

Static functions do not receive this argument, because they do not belong to some object. You can access statics like this:

print(AClass.someStaticField)
print(AClass.f())

You can even access statics using an object:

anObject = AClass()
print(anObject.someStaticField)
print(anObject.f())

The rest of their behaviour is similar to usual fields and methods

Chapter XVII: Dynamic class changing. System types' classes. Type customization. Underscore methods

So here is another concept - you can add functionality to your class even after it is defined. Consider Rect class from Chapter XIV:

class Rect {
    w = 0
    h = 0

    constructor (this, w, h) {
        this.w = w
        this.h = h
    }

    function area(this) {
        return this.w * this.h
    }
}

What if you suddenly want to add a perimeter() function to it? You can do it just by setting a field with target name to an anonymous function with target functionality:

Rect.perimeter = (this) -> { return (this.w + this.h) * 2 }

And there you have it. Of course, it's better to setup all functionality at once in class. And moreover I should say that you shouldn't use this construction unless absolutely necessary. But when can it be absolutely necessary? We'll talk about it in a little while. For now, let's learn about other concept:

Everything in Quail is an object

This means that everything in Quail has its own class? Yes, it does. We have Number, String, Bool, List, Dict and Object classes. Moreover, we have a Null class which represents the null value. You can do some instanceof experiments to see which value belongs to which class. But let's think about this for a moment: if everything in Quail is an object and thus has its class and we can dynamically add functionality to classes even after definition, doesn't this mean that we can add something to default Quail classes? And yes, it does. And this is the absolutely necessary case: you absolutely need to add something to a class, which declaration is not accessible. Let's see how we can do that:

Number.hello = "Hello!"
a = 3
print(a.hello)

And the magic happens! Furthermore, we can access a field just out of number:

Number.hello = "Hello!"
print(3.hello)

Actually, just out of any value:

Bool.promise = "I will call you later!"
print(false.promise)

We can do that even with Null:

Null.a = "A"
print(null.a)

Isn't it cool? We can even do some methods:

Number.add = (this, other) -> {
    return this + other
}
print(17.add(4))
Bool.inverse = (this) -> {
    return !this
}
print(false.inverse())

And the last concept I want to talk about is underscore methods. An underscore method is an utility method which helps Quail know what to do. For example: if I was to execute an addition on two objects, if addition was supported on these types (eg. Number + Number or String + String) it will be executed normally. But if that is not the case (eg. String ^ List) Quail will search for a specific method, in this case for _add method. If one will be found, operation will be delegated to it. If not, an UnsupportedOperationException will be raised (we'll talk about exceptions in next chapter). This way we can create custom operations. E.g. we can reverse a string using -:

String._neg = (this) -> {
    return this[::-1]
}
print(-"Hello")

The full table of operator and their corresponding methods is in the Quail Specification Chapter 6.

Chapter XVIII: Exceptions

Imagine that something in your program has gone wrong. It can be anything - computer is out of memory, invalid data inputted, website in unaccessible, etc. How do you report such events? Maybe you can just print the error message. But what if you want to immediatly stop execution of current function, when an error occurrs? Exceptions will come in handy.

An exception is an object, which carries data about the error, but most importantly it can be thrown. When you throw an exception (sometimes this process can be called raising an exception) it blasts through the code and stops the execution. An exception can be catched. If so, it will stop and execution will continue. Let's see an example:

print("Here it comes...")
throw Exception("Here I am!")
print("This will never be executed")

First line will be executed then on the second line exception will be created and thrown, thus stopping the flow and 3rd line will never be reached.

We can catch it:

try {
    print("Here it comes...")
    throw Exception("Here I am!")
    print("This will never be executed")
} catch {
    print("Caught!")
}

If come exception will occurr in try-block, it will be catched and passed into catch-block. We can receive it there:

try {
    print("Here it comes...")
    throw Exception("Here I am!")
    print("This will never be executed")
} catch as e {
    print("Caught! Message: " + e.message)
}

We can filter exceptions. Let's create our own exception first:

class MyException like Exception {
    constructor (this) {
        this.message = "I'm a custom exception"
    }
}


try {
    print("Here it comes...")
    throw MyException()
    print("This will never be executed")
} catch MyException as e {
    print("Caught! Message: " + e.message)
}


try {
    print("Here it comes...")
    throw Exception("Here I am!")
    print("This will never be executed")
} catch MyException as e {
    print("Caught! Message: " + e.message)
}

First try-block will be executed: print done, exception thrown. Then catch-block will be checked - it seeks for MyException - thrown exception is an instance of MyException, thus it's caught. In the second try-block we throw other exception, thus catch-block will not be executed and exception will not be caught.

Also, keep in mind that exceptions will blast through functions:

function haltAndCatchFire() {
    throw Exception("AAAAH!")
}

function callHalt() {
    haltAndCatchFire()
    print("This will never be executed")
}

print("Here it comes...")
callHalt()
print("This will never be executed")

Here: print done, callHalt being called, haltAndCatchFire being called, exception thrown, haltAndCatchFire interrupted and stopped and its caller - callHalt also interrupted and stopped, exception reaches main section and stops the code.

Practice 18.1

Modify our calculator (from Interchapter Practice 2) so it throws an exception when $R &lt; 0$

Do not delete this program, we'll return to it later.

Answer to 18.1 (only modifications are included):

function circumference() {
    R = num(input("R="))
    pi = 3.1415
    C = 2 * pi * R
    if R < 0
        throw Exception("R < 0")
    else
        print("C=" + string(C))
}

...

Chapter XIX: Variable modifiers. Method overloading

Another simple, yet useful concept: variable modifiers. Imagine a situation where you for example only want numbers passed into your function? How would you control that? Of course you can do:

function squared(n) {
    if not (n instanceof Number)
        throw Exception("Only numbers accepted!")
    return n^2
}

But that is bulky. Luckily Quail has a solution for you - variable modifier. It can be put before variable names (not before fields!) when you declare them or before argument names in functions. In both cases modifiers apply some restrictions to them. If we use modifiers in previous code, we get:

function squared(num n) {
    return n^2    
}

This little num will prevent any other value apart from numbers from getting into this function. If someone will try - they will get an exception. Here is a list of all type modifiers:

Modifier Meaning
num Only numbers (like Number)
string Only strings (like String)
bool Only booleans (like Bool)
void Only nulls (like Null)
Btw, who on earth will ever need it?
list Only lists (like List)
dict Only dictionaries (like Dict)
func Only dictionaries (like Dict)
notnull
required
Only non-null values (opposite to void)
object Any object
(Does not really do anything, because everything in Quail is an object)

There are also other modifiers:

Modifier Meaning
local Forces write to a local variable (More info about scopes in Quail Specification Chapter 7)
final Allows only one write to the variable after which it's locked
static Makes fields and methods static (You already know this one)

You can stack multiple modifiers:

local final string a = "Hello"

Or if you want for example only numbers and bools, you can create separate cases:

function negate(num | bool x) {
    if x instanceof Number
        return -x
    else if x instanceof Bool
        return not x
}

This way you can handle different data. But there is another, better way which is called method overloading. Essentialy, it means that if you declare a couple of functions with same names, but different argument modifiers then when you'll call it, the function will automatically resolve which code to call based on what types of arguments you pass:

function negate(num x) {
    return -x
}

function negate(bool x) {
    return not x
}

When you pass a number, the first function will be called and hen you pass a bool, the second. If you pass something different you'll get an exception. You can also declare the so-called fallback:

function negate(num x) {
    return -x
}

function negate(bool x) {
    return not x
}

function negate(x) {
    return null
}

In this case no exception will be thrown when you pass something apart from boolean and number, because there is a case which does not restrict types. These all cases are called alternative calls.

Chapter XX: Annotations and decorators

Annotations and decorators are advanced concepts which you won't use (at least at first) really often. So we'll go a bit faster here. Basically they modify language constructions (more specifically functions and classes) each in their own way.

Decorator is a script-defined component. Decorator is a function which recieves class or function and output their modified version. Here is an example:

function traceExecution(f) {
    return () -> {
        print("Start execution")
        f()
        print("End execution")
    }
}

@traceExecution
function sayHello() {
    print("Hello!")
}

sayHello()

Will output:

Start execution
Hello!
End execution

Basically, when sayHello function is created, it's original is passed into traceExecution which creates another function which does some its own business as well as calls the original function and this new function is then automatically places into sayHello.

Annotation is a language-defined component. This means you cannot create them. They also do some modifications. They are described in Quail Specifications. At the moment of writing of this chapter, only one annotation is available: @Deprecated. This can tell other developers that this functionality is deprecated, not good to use and maybe subjected to removal.

Chapter XXI: Using external libraries. Library lang/random

Not all functionality is accessible by default. Some is packaged into libraries. Library is a collection of externally defined functions, values and classes which you can use in your program. For example, if you want to generate some random numbers, you should use lang/random library. Let's see an example:

use "lang/random" = random
print(random.random())

Here, we use the library and place it into random variable. From here we can access its contents. The random function randomizes a real number between 0 and 1.

Chapter XXII: Creating libraries

In the previous chapter we've learned how to use libraries. Now we will learn how to create them.

Let's return back to our calculator and take the algorithm we've used to calculate the circumference and put it into a function:

function circumference(R) {
    return 2*3.14*R
}

We can create a library with this function like that:

  1. We create a new file (let's name it mathlib.q)
  2. We will then write this function in this file
  3. Then, at the very bottom of the file we will return a dictionary that contains our function

So we will get this:

function circumference(R) {
    return 2*3.14*R
}

return {"circumference": circumference}

Now, in the main file we will write this:

use "mathlib.q" = math
print(math.circumference(4))

And that will work! So basically what use command does is that it grabs a file that is specified in quotes, runs it, gets what it returns and whatever is returned is placed into the variable that is specified to the right of equals sign. What's more important is that the library's runtime retains and that means that library function can access other functions in the same library.

Another important thing to understand is that if a library was requested earlier, its return value will be cached and when it's requested another time it won't be ran again and the cached value will be automatically returned.

Chapter XXIII: Threading. Asynchronous code

The Quail language supports multithreading in two different ways.

The first is the canonical - explicit threading. You can create a thread using Thread class:

th = Thread(function(n) {
    through 0:n as i
        print(i)
}, [5])
th.start()

First argument is a function that will be run asynchronously, the second - list of arguments that will be passed to this function. More info about Thread API is available at Quail Core Docs

The second way to run asynchronous code is to use async keyword. If you add this keyword before some construction (for, while, through, function call, etc.) this line construction will be ran asynchronously. For example, if you want to create a perpetual background loop, you can write:

async while true {
     # some background code here
}

Chapter XXIV: Preprocessor

Also a topic that you won't really face that often. Preprocessor is a small program that goes through all your code before even parsing it and does some modifications. In particular (at the moment of writing) it can insert contents of an another file in executing file and replace text. Here are the examples:

Inserting:

In file code.q

print("Hello")

In main file:

#:include code.q
print("Hello 2")

After preprocessing, main file contents will be transformed into:

print("Hello")
print("Hello 2")

Contents of original file won't be harmed!

Replacement:

#:alias "SAY HELLO" print("Hello")
SAY HELLO

Will be transformed into

print("Hello")

Also supports regex:

#:alias "(SAY|TELL) HELLO TO (?<name>[a-zA-Z]*)" print("Hello, ${name}")
SAY HELLO TO Mark

Will be transformed into

print("Hello, Mark")

Chapter XXV: Variable references

This chapter will teach you about how values are stored in Quail.

You already know that every value in Quail is an object. So, how are they stored? Consider this example:

a = 2
b = 2

We create 2 then put it into a. That's what it looks like, but it's not quite right. Actually the 2 is created and just get thrown into heap. Then, we grab a link to that place in the heap and place the link into a.

In this example a and b contain links to separate objects, but what if:

a = 2
b = a

Now guess what? We have only one object in our heap. It's just links that are copied. a and b now store different links to the same object. We can proove it by:

a = 2
b = a
a.somefield = "Test message"
print(b.somefield)

As you can see we set some field to some message in a, but we can access it from b, because they refer to the same object. The same way argument passing happens:

function f(obj) {
    obj.somefield = "Test message"
}

a = 2
f(a)
print(a.somefield)

But what if you want to explicitly copy an object? Then there is a copy function:

a = 2
b = copy(a)
a.somefield = "Test message"
print(b.somefield)

Now it will print null, because variables now refer to separate objects.

Now let's consider this example:

a = [1, 2, 3]
b = copy(a)
a[0].somefield = "Test message"
print(b[0].somefield)

Run this example. You will see Test message appear. But why? We've copied the list!

The problem is that we've copied just the list. Not the values in it. The list is different, you can check it, but lists in a and b refer to same objects. So to copy an object and all of its contents (e.g. a list or a dict), use clone function:

a = [1, 2, 3]
b = clone(a)
a[0].somefield = "Test message"
print(b[0].somefield)

Now we'll see the null.

Chapter XXVI: Where to go further?

Congratulations!

You have made it, you've read all the Getting Started's 25 chapters. Now you can consider yourself a pro in Quail programming.

Now I recommend you save Quail Core Docs as a bookmark and look through it to learn about different built-in functions, classes and methods.

If you want to know even more about Quail, then you can proceed reading the Quail Specification

Thank you for using Quail!

⚠️ **GitHub.com Fallback** ⚠️