Skip to content

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:

    a 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 
    
    The compile-time 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:

    print(NInt is nom IntegralNumber) #! true 
    
    in this case is is evaluated at compile time and both is and is! 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:

    a Int64?
    b Int64
    c Dynamic
    print(a is ?) #! true
    print(b is ?) #! false 
    print(c is ?) #! false 
    print(a is @) #! false 
    
    note that a dynamic variable is intrinsicly nillable but the is operation only checks for explicitly declared nillable types.

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.