Skip to content

Variables

Syntax

All variables must start with either an underscoe _ or a lower case letter. variable names can include _, lower and upper case alphanumerics. _ itself is only a temporary variable. You can specify a type for a variable by adding it after the variable name. This follows the pattern as adding types for literals. The type in question can be auto or one of its variants, a path to a type or a function interface. For example:

/*
This is a variable with automatic type assignment. it will adopt the value 12
and if it is not already defined elsewhere the type NInt.
*/
a = 12 


b auto = 12 #! same as above but is defined here 

b strictly NInt = 12 #! same as above with explicit type declaration

b strictly global.NInt = 12 #! same as above

/*
This variable evaluates to a function interface instead of type that takes a
string and returns a 64bit integer.
*/
b (String) -> Int64 = my_function

#! Same as above but the interface is extern
b #extern; (String) -> Int64 = my_function

/*
This defines a strict function interface - more on these later
*/
b strictly (String) -> Int64 = my_function

Tip

Defining variables either explicitly or with the keyword auto will prevent conflation of the variable with a similarly named global variable and is a recommended practice.

Variables defined within a function are local variables. Variables outside of a function are global variables. You can define a lexical scope for both global and local variables by using curly braces:

a = 12  #! global variable

fun my_fun() {

  a += 5 #! modifies the global a

  b = 6 #! defines a local variable
}

{ #! defines a new lexical scope

  a auto = 12  #! defines a new global variable called `a` not accessible outside
}

Dynamic variables

To define a dynamic variable just set the type to Dynamic:

a Dynamic = 12
print(a) #! prints 12
a = `hello`
print(a) #! prints hello

a.unknown_method() #! compiles fine but generates an exception at runtime if executed

Dynamic variables do not participate in compile time type checking. For example calling a method that does not exists on a dynamic variable compiles just fine but generates a DynamicOperationException at runtime. This makes it more difficult to find issues with code as they are not caught by the compiler. Dynamic variables can therefore not participate in compile time optimizations either and as a result dynamic operations can run orders of magnitude slower than their statically typed counterparts.

Tuples

You can assign variables using tuples. For example to swap the values of a and b you can write:

a, b = b, a

you can also skip some of the assignments or create new variables in tuples:

a Int64, , b auto, _ = get_3_vals(), additional_val

You can unpack containers:

l = [`hello`, 12, 3.1]
a String, , b auto, _ = unpack! l, +Inf

print(a) #! prints hello
print(b) #! prints 3.10000000000000008882

Note

Unpacking is a dynamic operation which causes the type of b above to be set to Dynamic. Any Dynamic variable on the right hand side would also result in the same effect. Dynamic assignments happen at runtime and skip the static type checking step of the compiler. They also run slower as a result.

You can also define one variadic variable on the left hand side of an assignment:

l = [`hello`, 12, 3.1]
a String, *var, _ = unpack! l, +Inf

print(a) #! prints hello
print(var[0]) #! prints 12
print(var[1]) #! prints 3.10000000000000008882

var above will take on the type DynamicList.

Qualifiers

By default when defining a variable explicitly with a type, it is going to conform to that type structurally. For example variable x which is declared to be of Type1 can be assigned all Type1, Type2 and Type3 objects in the code below even though only Type2 has established a nominal is-a relationship with Type1:

type Type1 {

    p1 Int64
    p2 String
}

type Type2 inherits Type1 {

    #! will inherit p1 and p2
}

type Type3 {

    p1 Int64
    p2 String
}

x Type1 = Type1()  #! OK
x = Type2()  #! OK
x = Type3()  #! OK

This is because x is declared to structurally conform to Type1. This means that for the target type all methods must be structurally subtypes of Type1 and all properties the same so that using the interface of Type1 you can invoke those methods belonging to the target type e.g. Type3. We could however declare the variable x to accept only instances of types that nominally inherit from Type1. For example:

x nom Type1 = Type1()  #!OK
x = Type2()  #!OK
x = Type3()  #!Error

Eventhough Type3 above structurally conforms to Type1 its instances cannot be assigned to x as x accepts only nominally inherited types like Type2.

We can restrict the variable even further with the keyword strictly to demand exactly and only the type specified. This is the default behavior when no type is specified for a new variable or its type is set to auto:

x strictly Type1 = Type1()  #!OK
x = Type2()  #!Error
x = Type3()  #!Error

Constant variables

You can declare variables as const which will prevent assignment to the variables. It will not however make the variable immutable as mutability is not tracked in Gambol.

const x Int64 = 12

Note

Adding an exclamation mark after const (const!) will turn it into a const expression which is an entirely different concept.

The auto keyword

Speaking of the keyword auto, if you create an automatically typed variable but assign a nominal variable to it, the new variable is also going to have nominal as opposed to strict conformance to the target type. For example:

x nom Type1 = Type2() #! OK since Type2 inherits from Type1 nominally
y auto = x #! OK  y will be nom Type1 same as x
y = Type1() #! OK
y = Type2() #! OK
y = Type3() #! Error since Type3 is not nom Type1

in other words the auto keyword falls through from structural to nominal to strictly the target type depending on the right hand side of the assignment. It is only as strict as the right hand side is and not any more.

You can restrict auto to only go as far as nominal or structural even if the right hand side is a strict Type. You can do so by adding an exclamation mark to auto for nominal and two exclamation marks for structural:

x auto = Type1()   #! x will be strictly Type1
y auto! = Type1()  #! y will be nom Type1 even though right hand side is strictly Type1
z auto!! = Type1() #! z will be structurally Type1

What are strict function interfaces

Consider the variable x below and its declared type which is a function interface taking an integer and returning a string. All below functions can be assigned to x:

fun f1(a Int64) -> String { `hello` }
fun f2(a Int64, b Int16 = 12) -> String { `hello` }
fun f3(a Int64, b: String = `hi`) -> String { `hello` }

x (Int64) -> String = f1 #! OK
x (Int64) -> String = f2 #! OK
x (Int64) -> String = f3 #! OK

This is because all functions above can be called by passing just an integer and they all return a string.

For the purpose of subtyping, function parameters are contravaraint and return types are covariant. This means that if a function returns type A all subtypes of the function must either return A or a subtype of A and if a function takes an argument of type A all subtypes of the function must take the same argument of type A or a supertype of A.

If a function interface is declared strict however all subtypes must have exactly the same number of parameters and return types. In addition, parameters and return types with value and reference semantics must preserve that property. Value types must remain value types and reference types must remain reference types.

Comparison of qualifiers

The more strict a variable is the faster all operations on that variable become. This is because the compiler will have more precise information about the type assigned to the variable. Sometimes defining a strict variable is not possible. In those cases a nominal variable or a structural one can be considered. The table below summarizes the benefits and drawbacks of each level of strictness.

Note

scripting languages usually use Dynamic types and systems languages like C++ use nominal and strict types.

Pros Cons
Dynamic The variable can be assigned any type Very Slow
Structural Type checking at compile and run time. Can define new super types for third party types Sometimes but not always even worse performance than Dynamic since there might be an extra type checking step at runtime
Nominal Faster type checking both at runtime and compile time. Much faster execution at runtime restricted to explicit inheritance. You can't define new supertypes of types
Strict Fastest performance possible No runtime polymorphism

Nillable variables

Nil is a type that is not a subtype of any other type. The counterpart to Nil could be Any which is the nominal supertype of every type except Nil. Variables that are nillable can be assigned Nil to remove their content. To make a variable nillable add a question mark after the type specifier. if a nillable variable is assigned to a variable definition using the keyword auto the second variable will also be nillable. You can check if a variable is Nil using the is keyword.

a Int64? = 12

print(a == 12) #! true
print(a is Nil) #! false

a = Nil()

print(a == 12) #! false
print(a is Nil) #! true

b auto = a

print(b == 12) #! false
print(b is Nil) #! true

To mark a function interface as nillable add the question mark after the parameter list.

a (Int64)? -> String = Nil()
print(a is Nil) #! true
Nested and intrinsic nillables

Nested levels of nillables (nillable of nillable) are equivalent to just one level of nillable. In addition two types are intrinsicly nillable: Dynamic and Enums. This means that you can always assign Nil to a Dynamic variable or an Enum. Therefore nillable of Dynamic (Dynamic?) is just Dynamic and the same goes for Enums.

by reference nillables

Function parameters can be taken and return values can be returned by reference. If you make a by-ref parameter nillable it will read as a nillable reference to the target value. For example @Int? is a nillable reference to Int as opposed to a reference to a nillable Int.

Advanced Topics

Nillable checks

Nillable variables must go through a check at runtime each time they are accessed to see if the variable is Nil or not. In performance critical applications you can choose to bypass this check when accessing a member or calling the variable using the syntax below:

a SomeType? 
a.>property #! access property of SomeType
a.>(1,2) #! call a

Warning

This is an inherently unsafe operation and can lead to segmentation faults if the variable is Nil

Heap variables

You can define variables to be allocated on the heap instead of the stack. Heap variables are captured by reference by closures. To allocate a variable on the heap add the heap attribute to the variable definition:

#heap
a Int64

a = 12
print(a) #! prints 12

Reference variables

Variables can capture other variables by reference becoming essentially aliases for other variables or function return values. This is an inherently unsafe operation and therefore must be marked unsafe. Functions can capture parameters by reference also and by reference variables use the same syntax which is an @ before the type specifier but after any qualifiers:

#! function that captures by reference
fun add(a @Int64) -> Nil { a += 1 }

a = 12
add(a)
add(a)
print(a) #! prints 14

#! variable capturing by reference

#unsafe
b @auto = a

b *= 2
print(a) #! prints 28

Fat references

For types that have reference semantics and Dynamic variables the actual object is allocated on the heap. The variable itself however is allocated on the stack and contains two pointers. Same goes for variables containing functions though the actual function exists in the code area of the memory not on the heap. The two pointers all fat references contain are:

  1. A pointer to the AST node which describes the variable type. This is either a TypeView or a Function in most situations.

  2. A pointer to the actual object or the actual function.

you can get access to these pointers by acquiring a pointer to the variable and interpreting the pointer as an object of type GambolReference:

x = List[Int64]() #! List has reference semantics

print((pointer!(x) as GambolReference).ast_ptr) #! print an address e.g. 0x7FEECAA7AFC8
print((pointer!(x) as GambolReference).object) #! prints an address e.g. 0x7FEECD602B10