Miscellaneous
List Builder
You can use the list builder syntax to initialize lists, sets, dictionaries or
your own types. It works by initializing the type and repeatedly calling a
method to add each item to the list. By default if no types is specified list
builder will create a DynamicList
for single items and a Dict[Dynamic,
Dynamic]
for key value pairs.
l = [1,2,3,4,5]
d = [1: 12, `hi`: 15, `hello`: 3.14]
print(l)
print(d)
output
[1, 2, 3, 4, 5]
[1: 12, `hi`: 15, `hello`: 3.14000000000000012434]
Custom types
You could add a type along with its constructor arguments and the method used to add elements after the list builder:
[1,2,3,4,5]
[1,2,3,4,5] List[Int64]
[1,2,3,4,5] List[Int64](5)
[1,2,3,4,5] List[Int64](5) push
Unpacking
You can use unpack statements as elements in the list builder tuple. To unpack
single items use unpack!
and to unpack key value pairs use unpack!!
.
l = [300,400,500]
d = [`name`: `james`, `score`: 95]
print([1,2,3,unpack! l,4,5])
print([`age`: 40, `rank`: `senior`, unpack!! d])
output
[1, 2, 3, 300, 400, 500, 4, 5]
[`age`: 40, `rank`: `senior`, `name`: `james`, `score`: 95]
List and dictionary comprehensions
You can use inline for loops to generate a list of items or key value pairs to initialize dictionaries or other data types.
ks = [`first`, `second`, `third`, `fourth`, `fifth`]
vs = [x*x for x in 1..6]
d = [key: value for key, value in ks, vs]
print(d)
output
[`first`: 1, `second`: 4, `third`: 9, `fourth`: 16, `fifth`: 25]
To filter the items use the keyword keeping
:
print([x*x for x in 0..10 keeping x % 3 == 0]) #! prints [0, 9, 36, 81]
You could also use nested for loops as in the below example:
print([Pair(x, y) for x in ..10 for y in ..10 keeping x < y])
/*
This is equivalent to
list DynamicList
for x in ..10 {
for y in ..10 {
if x < y then list.add(Pair(x, y)) end
}
}
print(list)
*/
output
[(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (4, 5), (4, 6), (4, 7), (4, 8), (4, 9), (5, 6), (5, 7), (5, 8), (5, 9), (6, 7), (6, 8), (6, 9), (7, 8), (7, 9), (8, 9)]
as
and is
binary operations
is operator
The is
binary operation with its compile-time counterpart is!
have a
variety of use cases that are enumerated below. To negate the result of the is
operation you can write is not
and is! not
.
-
You can check if an expression is a subtype of a given type:
The compile-timea Dynamic = 233 print(a is NInt) #! true because this is a runtime check print(a is not NInt) #! false print(a is nom IntegralNumber) #! true
is!
would check if the given variable's static type conforms to the target rather than its value at runtime.a Dynamic = 233 print(a is! NInt) #! false because at compile a is just Dynamic not NInt print(a is! not NInt) #! true print(a is! nom IntegralNumber) #! false
-
You can also check if one type is a subtype of another type:
in this caseprint(NInt is nom IntegralNumber) #! true
is
is evaluated at compile time and bothis
andis!
are equivalent. -
You can also check if an expression or type is a subtype of a function interface:
print((Int=) -> String is () -> Dynamic) #! true print((Int=) -> String is strictly () -> Dynamic) #! false
-
Of course the type in question can be an enum but you can also check if the variable is a particular enum variant at runtime:
enum Color { red green blue } x Color = Color.red() print(x is Color) #! true print(x is Color.red) #! true print(x is Color.blue) #! false
-
You can also check if the variable or type on the left hand side is a type or a value type or a function.
f = NInt.fdiv print(f is type) #! false print(f is value type) #! false print(f is fun) #! true
-
To check if a variable or type identifier is nillable or by-reference follow the example below:
note that a dynamic variable is intrinsicly nillable but the is operation only checks for explicitly declared nillable types.a Int64? b Int64 c Dynamic print(a is ?) #! true print(b is ?) #! false print(c is ?) #! false print(a is @) #! false
Note
Checking for nominal subtypes is much faster than checking for structural subtypes as it does not require analyzing the structure of the type at runtime. For the same reason checking for strict subtypes is the fastest.
as-type operator
The as
and as!
operators are used for up and down casting of polymorphic
types as well as interpreting pointers. If it is possible to reinterpret the
left hand side of an as-type operation without allocating new space then as
should be used for this purpose. If on the other hand new space is required to
convert the variable from one type to another and store the results in as!
must be used. If necessary the compiler will do a runtime check to see if the
types are compatible with each other and if not it will throw an
AsOperationException
. When the left hand side of as-type operation is a
pointer there will not be any runtime check and the pointer will be blindly
interpreted as the target type.
Info
Gambol does not store the polymorphic and virtual function information and
tables of each type in the object like C++
and some other languages do.
It rather stores a pointer to that information in each nominal variable!
As such up and down casting of nominal variables require allocating new
space. Down casting to value types however does not need new space as
value types are not polymorphic and the variables of value types point
directly to the object itself.
Some examples of as-type operation:
a Any = 123 Int128
print(a as Int128) #! 123
print(a as IntegralNumber) #! 123
print(a as! nom IntegralNumber) #! 123
b nom Any = 123 Int128
print(b as Int128) #! 123
print(b as! IntegralNumber) #! 123
print(b as! nom IntegralNumber) #! 123
c Int64 = 1000
print(pointer!(c) as UInt8) #! 232
print(pointer!(c) as Int16) #! 1000
Note
b as Int128
above does not need space to be allocated as Int128 is a value
type.
Modules
You normal write code in a file with .gambol
extension. This file becomes the
main module by the compiler. You can also separate code into different modules
and then use several ways of importing those to use their code. Each module
must sit in a folder with the same name as module and there must be a file in
that folder with the same name as the module and with extension .gambol
. This
folder then must be accessible to the compiler either through the compiler flag
-I
or the environment variable GAMBOL_LIB
.
Imports
You can import a module by writing import <my-module>
. If you import the
module multiple times throughout the code it will actually be imported only
once but the symbols become available at the site you did the importing as
applicable. By default a simple import <my-module>
only creates the
<my-module>
symbol which you can use to access other symbols within that
module.
import random
gen random.Squares64()
print(random.Random.float[Float32](gen)) #! 0.432477056980133056641
If you don't want to reference a module with its name you can use the keyword
as
when importing the module.
import random as r
gen r.Squares64()
print(r.Random.float[Float32](gen)) #! 0.274949669837951660156
You can also choose to import the symbols you use from the module:
from random import Squares64, Random
gen Squares64()
print(Random.float[Float32](gen)) #! 0.920569419860839843750
Or you can use a wildcard import to import every public symbol:
from random import *
gen Squares64()
print(Random.float[Float32](gen)) #! 0.200087398290634155273
public symbols are functions whose names do not start with __
and types
whose names don't end with __
.
Runtime effect
When a module is imported to the program the contents of the module get
executed at runtime at the site of import and this process only happens once.
The compiler provides two special variables __module__
and __main__
that
point to the current module's AST node and the main modules AST node. You could
use these if prevent code from executing if the module is not the main module
e.g. when it is imported by another application.
print(`hello from this module`) #! will always be executed
if __module__ == __main__ then
print(`running this module as the main application`)
end
Embedded imports
Sometimes you don't want to separate the code into different modules
necessarily but would rather create a single module with its code separated
into different files for orginizational purposes. In those cases you could
use the embedded import syntax import!
to directly insert the contents of the
files where they are embedded. The files must sit in the same directory as the
module's main .gambol
file or subdirectories within. To import a file from
a subdirectory you must separate the path with a dot:
import! my_functions
import! my_types.this_type
import! my_types.that_other_type
Warning
As embedded imports are directly inserted wherever they are used multiple embedded imports will result in duplicated code and they are also susceptible be the problem of circular imports. You'd have to organize your files occordingly to avoid these issues.