added the ascii version of the docs
This commit is contained in:
+443
@@ -0,0 +1,443 @@
|
|||||||
|
|
||||||
|
# ===================================================== #
|
||||||
|
# === EXPRESSIONS ===================================== #
|
||||||
|
# ===================================================== #
|
||||||
|
|
||||||
|
## Basics ##
|
||||||
|
|
||||||
|
Expression use infix notation. You can have expressions
|
||||||
|
of numeric values, boolean values and other datatypes like
|
||||||
|
strings of text.
|
||||||
|
|
||||||
|
When expressions are used as statements, they need to be
|
||||||
|
terminated using a semicolon. Here's an example:
|
||||||
|
|
||||||
|
1 | 2 * (1 + 2);
|
||||||
|
|
||||||
|
The basic values that can be used are integers, floats,
|
||||||
|
booleans and "none".
|
||||||
|
|
||||||
|
|
||||||
|
## Integers, floats and arithmetic operators ##
|
||||||
|
|
||||||
|
Both integers and floats (floating point values) are
|
||||||
|
signed and represented using 8 bytes. Integers can
|
||||||
|
represent integer values between [2^61-1, 2^61], like
|
||||||
|
"int64_t"s in C/C++. On the other hand, floats are
|
||||||
|
equivalent to C/C++ "double"s. Numeric values can be
|
||||||
|
operated onto using the arithmetic operators:
|
||||||
|
|
||||||
|
- addition "+" (binary and unary)
|
||||||
|
- subtraction "-" (binary and unary)
|
||||||
|
- multiplication "*"
|
||||||
|
- division "/"
|
||||||
|
- modulo "%"
|
||||||
|
|
||||||
|
Here "modulo" refers to the remainder of the division.
|
||||||
|
These operations mainly behave like one would expect and
|
||||||
|
have the following type conversion rules:
|
||||||
|
|
||||||
|
- Operations involving integers evaluate to integers, except
|
||||||
|
division. The result of a division is always a float.
|
||||||
|
|
||||||
|
- If an arithmetic operation involves a float, the result is
|
||||||
|
also float.
|
||||||
|
|
||||||
|
If operations on integers overflow or underflow, the
|
||||||
|
program's execution is aborted.
|
||||||
|
|
||||||
|
## Booleans and logical operators ##
|
||||||
|
|
||||||
|
Boolean values are values that can either be "true" or
|
||||||
|
"false". They have the property that the logical negation
|
||||||
|
of one equals the other.
|
||||||
|
|
||||||
|
Booleans can be operated onto using logical operators such
|
||||||
|
as "and", "or" and "not". These operators expect boolean
|
||||||
|
values and return a new boolean value. You probably know
|
||||||
|
these well, but for completeness sake, here's how they
|
||||||
|
work:
|
||||||
|
|
||||||
|
1 | true and true; # = true
|
||||||
|
2 | true and false; # = false
|
||||||
|
3 | false and true; # = false
|
||||||
|
4 | false and false; # = false
|
||||||
|
5 |
|
||||||
|
6 | true or true; # = true
|
||||||
|
7 | true or false; # = true
|
||||||
|
8 | false or true; # = true
|
||||||
|
9 | false or false; # = false
|
||||||
|
10 |
|
||||||
|
11 | not true; # = false
|
||||||
|
12 | not false; # = true
|
||||||
|
|
||||||
|
If any of the operands aren't booleans, the program's
|
||||||
|
execution is aborted.
|
||||||
|
|
||||||
|
Operators "and" and "or" are short-circuit operators. This
|
||||||
|
means that they only consider the right operand if they can't
|
||||||
|
deduce the solution from the left one. For example, if the
|
||||||
|
left operand of an "and" evaluates to "false", it isn't
|
||||||
|
necessary to evaluate the right one, since the result of the
|
||||||
|
overall operation can only be "false".
|
||||||
|
|
||||||
|
|
||||||
|
## Relational operators ##
|
||||||
|
|
||||||
|
Relational operators are those which evaluate to booleans
|
||||||
|
and take, in general, non-boolean operands. They are:
|
||||||
|
|
||||||
|
- equal "=="
|
||||||
|
- not equal "!="
|
||||||
|
- less than "<"
|
||||||
|
- less than or equal "<="
|
||||||
|
- greater than ">"
|
||||||
|
- greater than or equal ">="
|
||||||
|
|
||||||
|
Equal (not equal) can be applied to all type of operands
|
||||||
|
and return true (false) when the operands have the same
|
||||||
|
type and hold the same value. These operator don't allow
|
||||||
|
funny business like 1 == "1" evaluating to "true".
|
||||||
|
|
||||||
|
The remaining relational operators are applied to numeric
|
||||||
|
datatypes (ints and floats).
|
||||||
|
|
||||||
|
|
||||||
|
## The none value ##
|
||||||
|
|
||||||
|
The none value used to represent the void of a value and
|
||||||
|
has the only property of being equal to itself and itself
|
||||||
|
only. You can use the none value using the "none" keyword:
|
||||||
|
|
||||||
|
1 | x = none;
|
||||||
|
|
||||||
|
|
||||||
|
## Variables and assignments ##
|
||||||
|
|
||||||
|
You can store computed values into variables in order to
|
||||||
|
reuse them later on. Variables are created using the
|
||||||
|
assignment operator:
|
||||||
|
|
||||||
|
1 | x = 1 + 4;
|
||||||
|
2 | y = x + 2;
|
||||||
|
|
||||||
|
here we're assigning to the variable "x" the number 5 then,
|
||||||
|
we're assigning to "y" the value 7 by accessing the value
|
||||||
|
previously stored into "x".
|
||||||
|
The left operand of the assignment operator must be a variable
|
||||||
|
name while the right operator can have any type.
|
||||||
|
|
||||||
|
Variable names can consist of digits, letters or underscores,
|
||||||
|
but the first character can't be a digit though.
|
||||||
|
|
||||||
|
Since the assignment operator is an operator, other than
|
||||||
|
do an assignment it also returns a value. Any assignment
|
||||||
|
evaluates to it's right operand. By instance
|
||||||
|
|
||||||
|
1 | y = (x = 1 + 4) + 2;
|
||||||
|
|
||||||
|
this expression will result in "x" having value 5 and "y"
|
||||||
|
value 7. This is because "x = 1 + 4", other than assigning
|
||||||
|
to "x", is equivalent to writing "5".
|
||||||
|
|
||||||
|
|
||||||
|
## Composit types and square bracket notation ##
|
||||||
|
|
||||||
|
Composit values are collections of other values, which may
|
||||||
|
also be composite. The composite types are "List", "Map" and
|
||||||
|
"String". For all collection types it's possible to insert
|
||||||
|
and retrieve values using the "[]" notation:
|
||||||
|
|
||||||
|
1 | coll[key] = item; # Store the value associated to the
|
||||||
|
2 | # variable "item" with key "key" in
|
||||||
|
3 | # the collection "coll".
|
||||||
|
4 |
|
||||||
|
5 | item = coll[key]; # Get the item back by selecting it
|
||||||
|
6 | # using it's key
|
||||||
|
7 |
|
||||||
|
|
||||||
|
In this example, the "coll" variable is a collection type,
|
||||||
|
while the types of "key" and "item" depend on the type of
|
||||||
|
collection.
|
||||||
|
|
||||||
|
|
||||||
|
## Lists ##
|
||||||
|
|
||||||
|
Lists are heterogeneous and ordered collections of values.
|
||||||
|
Each item they contain is associated to it's position in
|
||||||
|
the list (the first element has position 0). They're defined
|
||||||
|
and used with the following syntax:
|
||||||
|
|
||||||
|
1 | my_list = [true, 1.2, 19];
|
||||||
|
2 |
|
||||||
|
3 | x = my_list[0]; # true
|
||||||
|
4 | y = my_list[2]; # 19
|
||||||
|
5 |
|
||||||
|
6 | my_list[0] = 13;
|
||||||
|
7 |
|
||||||
|
8 | z = my_list[0]; # z is 13 now!
|
||||||
|
|
||||||
|
Trying to access an item using as key something which isn't
|
||||||
|
an integer or an in range integer (less then 0 or higher
|
||||||
|
than the length of the array minus one) will result in an
|
||||||
|
error. The only exception to this rule is the index equal
|
||||||
|
to the item count of the list (which is out of bounds since
|
||||||
|
the last item of the list has index equals to the item count
|
||||||
|
minus one): by inserting a value at this index, the list
|
||||||
|
will increase it's size by 1. There isn't a limit on how
|
||||||
|
many values a list can contain.
|
||||||
|
|
||||||
|
|
||||||
|
## Strings ##
|
||||||
|
|
||||||
|
Strings are values which contain UTF-8 encoded text.
|
||||||
|
A string can be instanciated placing text between single or
|
||||||
|
double quotes:
|
||||||
|
|
||||||
|
1 | "I'm a string!";
|
||||||
|
2 |
|
||||||
|
3 | 'I am too!';
|
||||||
|
4 |
|
||||||
|
|
||||||
|
Special character (such as horizontal tabs and carriage
|
||||||
|
returns) can be specified using the "\x" notation:
|
||||||
|
|
||||||
|
"\t" - tab
|
||||||
|
"\r" - carriage return
|
||||||
|
"\n" - newline
|
||||||
|
|
||||||
|
When strings contain quotes that match the ones surrounding
|
||||||
|
them or the "\" character, it's necessary to escape them:
|
||||||
|
|
||||||
|
1 | 'Hi, I\'m Francesco!';
|
||||||
|
2 | "Hi \"Francesco\", how old are you?";
|
||||||
|
3 | "This is a backlash \\ and you can do nothing about it";
|
||||||
|
|
||||||
|
Like arrays, single characters can be selected referring to them
|
||||||
|
by their position relative to the first character using the
|
||||||
|
"[]" notation. When selecting single characters from a string,
|
||||||
|
they're returned as new strings.
|
||||||
|
|
||||||
|
Once a string is created, it's not possible to modify it.
|
||||||
|
If you want to change a string's value, you need to create
|
||||||
|
a new updated version of the string.
|
||||||
|
|
||||||
|
|
||||||
|
## Maps and the dot operator ##
|
||||||
|
|
||||||
|
Maps are collections of key-value pairs, where both keys and
|
||||||
|
values can have any type.
|
||||||
|
The syntax for defining and using maps is this:
|
||||||
|
|
||||||
|
1 | me = {"name": "Francesco", "age": 24};
|
||||||
|
2 |
|
||||||
|
3 | my_name = me["name"]; # Francesco
|
||||||
|
4 |
|
||||||
|
5 | me["name"] = true;
|
||||||
|
6 |
|
||||||
|
7 | my_name = me["name"]; # true
|
||||||
|
|
||||||
|
When selecting from a map a value associated to a key which
|
||||||
|
was never inserted, "none" is returned:
|
||||||
|
|
||||||
|
1 | my_map = {1: "one", 3: "three"};
|
||||||
|
2 | two = my_map[2]; # none
|
||||||
|
|
||||||
|
Because of the existence of compount statements, expression
|
||||||
|
statements can't start with the "{" token. The parser would
|
||||||
|
assume it's a badly formatted compount statement. To avoid
|
||||||
|
the ambiguity, you can add some tokens that have no effect
|
||||||
|
before the "{":
|
||||||
|
|
||||||
|
1 | {"day": "Monday"}; # invalid
|
||||||
|
2 | +{"day": "Monday"}; # valid
|
||||||
|
3 | ({"day": "Monday"}); # also valid
|
||||||
|
|
||||||
|
This isn't very pretty but it's a case that doesn't occur
|
||||||
|
in practice.
|
||||||
|
|
||||||
|
When instantiating a map, when a key is a string that follows
|
||||||
|
variable name rules, the encoling quotes can be dropped:
|
||||||
|
|
||||||
|
1 | # These are equivalent
|
||||||
|
2 | +{"name": "Francesco", "age": 25};
|
||||||
|
3 | +{name: "Francesco", age: 25};
|
||||||
|
|
||||||
|
If instead you wanted to use the variable named "name" as a
|
||||||
|
key, you can do that by adding some redundancy:
|
||||||
|
|
||||||
|
1 | name = "x";
|
||||||
|
2 |
|
||||||
|
3 | # These are equivalent
|
||||||
|
4 | +{(name): "Francesco"};
|
||||||
|
5 | +{ +name: "Francesco"};
|
||||||
|
6 | +{"x": "Francesco"};
|
||||||
|
7 |
|
||||||
|
8 | # And are different from these
|
||||||
|
9 | +{name: "Francesco"};
|
||||||
|
10 | +{"name": "Francesco"};
|
||||||
|
|
||||||
|
Similarly, when querying a map for an item associated to a
|
||||||
|
string that follows variable name rules, you can use the dot
|
||||||
|
operator:
|
||||||
|
|
||||||
|
1 | # These are equivalent
|
||||||
|
2 | me["name"] = "Francesco";
|
||||||
|
3 | me.name = "Francesco";
|
||||||
|
|
||||||
|
|
||||||
|
## Function calls ##
|
||||||
|
|
||||||
|
We haven't seen how function definitions work yet, but you
|
||||||
|
can imagine they work like other languages such as Python
|
||||||
|
or JavaScript for now. Assuming we defined a function named
|
||||||
|
"sayHello", we can call it using the usual "()" notation:
|
||||||
|
|
||||||
|
1 | sayHello();
|
||||||
|
2 | sayHello(1);
|
||||||
|
3 | sayHello(1, 2, 3);
|
||||||
|
|
||||||
|
|
||||||
|
## Functions useful for collections ##
|
||||||
|
count, keysof
|
||||||
|
|
||||||
|
# ===================================================== #
|
||||||
|
# === FUNCTIONS ======================================= #
|
||||||
|
# ===================================================== #
|
||||||
|
|
||||||
|
## Definition and basics ##
|
||||||
|
|
||||||
|
Functions are defined like this:
|
||||||
|
|
||||||
|
1 | fun sayHello() {
|
||||||
|
2 | print("Hello!\n");
|
||||||
|
3 | }
|
||||||
|
|
||||||
|
they must always start with the "fun" keyword followed
|
||||||
|
by the function's name. Function name rules are the same
|
||||||
|
as variables: they can contain letters, numbers or
|
||||||
|
underscores, but the first character can't be a number.
|
||||||
|
One or more arguments can be provided by specifying
|
||||||
|
their names between the "()" following the name:
|
||||||
|
|
||||||
|
1 | fun sayHelloTo(name1, name2) {
|
||||||
|
2 | print("Hello ", name1, " and ", name2, "!\n");
|
||||||
|
3 | }
|
||||||
|
|
||||||
|
When a function is called with more arguments than the
|
||||||
|
ones specified in it's definition, the extra ones are
|
||||||
|
discarded. If less arguments than expected are provided,
|
||||||
|
the remaining ones are "none" by default.
|
||||||
|
It's possible to specify default values for any of the
|
||||||
|
arguments. When the caller passes "none" as an argument,
|
||||||
|
the default value is used:
|
||||||
|
|
||||||
|
1 | fun sayHelloTo(name1="Francesco", b="Giovanni") {
|
||||||
|
2 | print("Hello ", name1, " and ", name2, "!\n");
|
||||||
|
3 | }
|
||||||
|
4 |
|
||||||
|
5 | sayHello(none, "Filippo"); # Hello Francesco and Filippo!
|
||||||
|
6 |
|
||||||
|
7 | # Here the arguments are implicitly none
|
||||||
|
8 | sayHello(); # Hello Francesco and Giovanni!
|
||||||
|
|
||||||
|
Return value can be specified using the "return" keyword:
|
||||||
|
|
||||||
|
1 | fun sum(a, b) {
|
||||||
|
2 | return a + b;
|
||||||
|
3 | }
|
||||||
|
4 |
|
||||||
|
5 | print("The sum of 3 and 7 is ", sum(3, 7), "\n");
|
||||||
|
|
||||||
|
functions that don't return a value explicitly will
|
||||||
|
return "none" implicitly.
|
||||||
|
Multiple values can be returned. To get the extra return
|
||||||
|
values, one must assign them to variables. If called outside
|
||||||
|
of an assignment, only the first return value is considered.
|
||||||
|
|
||||||
|
1 | fun divmod(x, y) {
|
||||||
|
2 | return (x / y), (x % y);
|
||||||
|
3 | }
|
||||||
|
4 |
|
||||||
|
5 | res1, res2 = divmod(100, 20); # 5, 0
|
||||||
|
6 |
|
||||||
|
7 | print(divmod(100, 20)); # Prints 5 (the modulo's result is discarded)
|
||||||
|
|
||||||
|
When a function returns more values than what was expected
|
||||||
|
in an assignment expression, the extra values are ignored.
|
||||||
|
If an assignment expects a function to return more values
|
||||||
|
than it actually returns, the extra variables are set to
|
||||||
|
"none".
|
||||||
|
|
||||||
|
## Scoping and closures ##
|
||||||
|
|
||||||
|
Functions are the only construct that creates a new scope:
|
||||||
|
when variables are defined inside a functions, they're only
|
||||||
|
accessible from within that function. When the function
|
||||||
|
returns, all variables defined by it are no longer accessible.
|
||||||
|
Here are some examples:
|
||||||
|
|
||||||
|
1 |
|
||||||
|
2 | fun doSomething() {
|
||||||
|
3 | name = "Francesco";
|
||||||
|
4 | }
|
||||||
|
5 |
|
||||||
|
6 | doSomething();
|
||||||
|
7 | print(name); # Runtime Error: No variable "name" is defined!
|
||||||
|
|
||||||
|
By contrast, functions can access variables defined in their
|
||||||
|
parent scope (relative to their definition)
|
||||||
|
|
||||||
|
1 | name = "Francesco";
|
||||||
|
2 | age = 24;
|
||||||
|
3 |
|
||||||
|
4 | fun printVars() {
|
||||||
|
5 | print(name, age);
|
||||||
|
6 | }
|
||||||
|
7 |
|
||||||
|
8 | printVars(); # prints: Francesco24
|
||||||
|
|
||||||
|
An alternative way of saying this is that functions create
|
||||||
|
closures. This mechanism also works recursively, functions
|
||||||
|
defined within other functions can access both the parent
|
||||||
|
function's variables and global variables.
|
||||||
|
|
||||||
|
1 | X = 1;
|
||||||
|
3 |
|
||||||
|
4 | fun wrapper() {
|
||||||
|
5 |
|
||||||
|
6 | Y = 2;
|
||||||
|
7 |
|
||||||
|
8 | fun printVars() {
|
||||||
|
9 | print(X, Y);
|
||||||
|
10 | }
|
||||||
|
11 |
|
||||||
|
12 | printVars();
|
||||||
|
13 | }
|
||||||
|
14 |
|
||||||
|
15 | wrapper(); # prints: 12
|
||||||
|
|
||||||
|
A cool implication of closures is the ability to create
|
||||||
|
parametric definitions of functions. This is done by
|
||||||
|
returning from a function a function defined inside it
|
||||||
|
|
||||||
|
1 | fun createDivisibilityCheck(n) {
|
||||||
|
2 | fun isDivisibleByN(k) {
|
||||||
|
3 | return k % n == 0;
|
||||||
|
4 | }
|
||||||
|
5 |
|
||||||
|
6 | return isDivisibleByN;
|
||||||
|
7 | }
|
||||||
|
8 |
|
||||||
|
9 | isDivisibleBy2 = createDivisibilityCheck(2);
|
||||||
|
10 | isDivisibleBy3 = createDivisibilityCheck(3);
|
||||||
|
11 |
|
||||||
|
12 | isDivisibleBy2(100); # true
|
||||||
|
13 | isDivisibleBy2(107); # false
|
||||||
|
14 |
|
||||||
|
15 | isDivisibleBy3(39); # true
|
||||||
|
16 | isDivisibleBy3(100); # false
|
||||||
|
|
||||||
|
in this example, "isDivisibleBy2" and "isDivisibleBy3"
|
||||||
|
share their implementation. What changes, is the closure
|
||||||
|
of their parent scopes.
|
||||||
Reference in New Issue
Block a user