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:
-
A pointer to the AST node which describes the variable type. This is either a
TypeView
or aFunction
in most situations. -
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