Functions
Basics
Use the keyword fun
to define a function:
fun my_fun(a Int64) -> String {
`received argument ` a
}
print(my_fun(12)) #! prints received argument 12
Named Parameters
you can also define named parameters after positional ones. Named parameters usually take on the role of configuration options whereas positional parameters are usually the main operands the function works with.
fun my_fun(a Int64, b Int64, k1: String, k2: String) -> String {
`received argument ` a ` and k2: ` k2
}
print(my_fun(12, 14, k2: `hello`, k1: `hello`)) #! prints received argument 12 and k2: hello
Multiple return values
As you can see the last expression in the function is implicitly returned. To
explicitly return a value use the return
keyword.
A function can also return multiple values:
fun my_fun() -> Int64, String, Float, DynamicList {
return 12, `hello`, 44.333, []
}
print(len!(my_fun())) #! prints 4
print(my_fun()[0]) #! prints 12
print(my_fun()[1]) #! prints hello
print(my_fun()[2]) #! prints 44.3329999999999984084
print(my_fun()[3]) #! prints []
Note
len!
above will return the length of the tuple as determined at compile
time not runtime. For example len!(my_list)
is going to be 1 not to be
confused with my_list.len()
A function returning Dynamic
can also return multiple values:
fun my_fun() -> Dynamic {
return 12, `hello`, 44.333, []
}
a, b, c, d = my_fun()
print(a) #! prints 12
print(b) #! prints hello
print(c) #! prints 44.3329999999999984084
print(d) #! prints []
Optional parameters
Use =
to assign default values to parameters both named and positional. This
makes them optional when calling. For positional parameters, optional
parameters must come after required parameters. All default values are
evaluated when calling the function rather than at the definition site.
fun my_fun(a Int64 = 100, config: String = `fast`) -> String {
`a: ` a `, config: ` config
}
print(my_fun()) #! prints a: 100, config: fast
For positional parameters, you can skip over some optional ones and not others when making calls. Named arguments can be specified in any order obviously.
fun my_fun(a Int64 = 100, b Float = 3.14, c DynamicList?, d String = ``) -> String {
`a: ` a `, b: ` b `, c: ` c `, d: ` d
}
print(my_fun(,,,`hello`))
#! prints a: 100, b: 3.14000000000000012434, c: Nil, d: hello
parameter c
above is Nillable and therefore is considered optional even if
it does not have a default value. All Enums and Nillables default to Nil.
Variadic args
Can be defined with a star before the parameter name for positional parameters
and two stars for named parameters. Variadic args must come last in the list of
positional arguments. Same rule applies for named arguments. The type for
variadic args is DynamicList
and the type of variadic named args is
Dict[String, Dynamic]
.
fun my_fun(a Int64, *var_args, conf: String, **extra) -> Nil {
print(`a: ` a `, conf: ` conf)
print(var_args)
print(extra)
}
my_fun(12, 3.14, +Inf, os_name: `linux`, uptime: 288, conf: `default`)
output
a: 12, conf: default
[3.14000000000000012434, +Inf]
[`os_name`: `linux`, `uptime`: 288]
Automatic return type
You can use the keyword auto
or skip specifying the return type of a function
altogether. In these cases the return type will be inferred from the first
return statement used in the body of the function. If inference fails an error
is generated at compile time and you'll have to specify the return type
explicitly.
fun my_fun() -> auto { 23 }
fun my_fun2() { 23 }
print(my_fun()) #! prints 23
print(my_fun2()) #! prints 23
Closures
Closures are capable of capturing (closing over) local variables from within
the functions they are defined in. By default a closure obtains a copy of the
variable captured but variables that are declared to reside on the heap (using
the #heap
attribute) will be captured by reference. To Define a closure
simply add an exclamation mark after the keyword fun
:
fun get_mul_by(lhs Int64) -> strictly (Int64) -> Int64 {
fun! product_closure(rhs Int64) -> Int64 {
lhs * rhs #! lhs is captured by copying from get_mul_by
}
return product_closure
}
mul_by_10 = get_mul_by(10)
print(mul_by_10(2)) #! prints 20
print(mul_by_10(4)) #! prints 40
Decorators
Proxy decorators are functions that take one function and return another function. There is syntactic sugar for passing functions through proxy decorators as illustrated in the snippet below:
fun transform(f (Int64) -> Int64, a Int64, b Int64) -> strictly (Int64) -> Int64 {
fun! new_f(x Int64) -> Int64 {
a * f(x) + b
}
return new_f
}
@transform(2, 100)
fun shift(x Int64) -> Int64 {
x + 5
}
print(shift(10)) #! prints 130
In the above code normally shift(10) would just add 5 to its argument and
return 15. However the function is passed to the transform decorator which
wraps the function in a formula new_f(x) = a * f(x) + b
and returns the new
function. The transform decorator takes a function and two arguments a and b
that determine the nature of the transformation. In the above code a and b are
2 and 100. As a result shift(10) becomes 2 * (10+5) + 100 = 130
.
This feature could be useful across many areas from framework design to targeting custom hardware.
Pass and return by reference
By default arguments are passed to a function by copying and return types are copied as well. For variables with value semantics the value is copied and for variables with reference semantics the reference itself is copied. Passing an argument by reference on the other hand means that the function will gain access to exactly the same variable passed to it and can modify the contents of that variable if need be such that it will be visible from outside the function. However it is inherently an unsafe operation as the object passed to the function could have been destroyed elsewhere or unknowingly by the function itself making access to the object a memory hazard. As for return values, returning by reference is even more dangerous as one could absent-mindedly return local variables by reference which are destroyed before the function exits and therefore lead to segmentation faults.
There have been many strategies implemented to mitigate or remove the risks of pass by reference in systems programming languages before. Gambol takes a middle ground approach between safety and complexity. Pass and return by reference operations in Gambol are considered unsafe however Gambol doesn't really allow return by reference until the value in question has a global lifetime. Currently the only two things that have global lifetimes are global variables and as-type operations with pointers on the left hand side. This will force the programmer to use pointers to return by reference which is unsafe territory and demands extra attention and vigilance. At the same time it also makes it possible to write functions that can return a mixture of by-value and by-reference items and in former case using normal syntax alleviating the cognitive load to some extent. In other words if you are using pointers you must know exactly what you are doing and if not well you won't get to return most things by reference.
To mark a parameter, named parameter or return type as by-reference add an @
sign before the type specifier:
fun my_fun(a @Int64, b Int64) -> Nil { a += b }
a = 12
my_fun(a, 10)
print(a) #! prints 22
Tip
In a function that takes two or more parameters in which at least one of them is taken by reference watch out if modifying one parameter may lead to the destruction of another by-ref parameter and take measures accordingly.
fun get_third_item(list List[Int64]) -> @Int64 {
return list.get_element_ptr(2) as Int64
}
l = [1,2,3,4,5] List[Int64]
get_third_item(l) += 100
print(l) #! prints [1, 2, 103, 4, 5]
here is what happens if we don't use an as-type
operation with pointer in the
above code.
fun get_third_item(list List[Int64]) -> @Int64 {
return list[2]
}
l = [1,2,3,4,5] List[Int64]
get_third_item(l) += 100
print(l) #! prints [1, 2, 3, 4, 5]
The third item in the list is returned by value and as a result only a copy of
it gets incremented by 100. The list itself and its third item remain intact.
This is despite the fact that list[2] itself returns the item by reference. For
example list[2] += 100
would actually increment the item in the list by 100.
However it does not have a global lifetime and therefore when assigned to a
function return the item gets copied over.
Coroutines and Async/Await
The async
library provides the data structures necessary to perform
cooperative multitasking using coroutines and futures. This is mostly used in
applications that require massively parallel data processing on a limited
number of threads such as web applications receiving thousands of requests
each second that cannot afford to spawn a new thread for each request.
Raw coroutines
A future is an object that is to eventually hold a value. It has functions to help wait for the value while it has not arrived and check it out once it does. Here is one interface for future as defined in the async library at the time of writing this:
#sealed
type Future {
fun wait(s Self) -> Nil
fun signal(s Self) -> Nil
fun get_value(s Self) -> Dynamic
cast to Bool (s Self)
value Dynamic
exception nom Exception?
has_value Bool
}
A coroutine is a callable object (that takes no parameter and returns Dynamic)
that is derived from the Coroutine
type. It has as a result a future that you
can grab to check if it is done running or not. To actually run the coroutine
the async library provides an event loop that takes in and runs coroutines as
they arrive asynchronously (in other threads and in parallel). Each coroutine
itself can add to the loop and wait on more coroutines to be scheduled for
running and so on since each coroutine contains a reference to the loop itself
too. Here is the interface for a Coroutine object:
type Coroutine {
#strict
(s Self) -> Dynamic
future Future
is_generator Bool
__loop AsyncLoop?
}
Here is one way of explicitly creating an event loop, defnining and running a
new coroutine using the async
library.
from async import *
#sealed
type MyCoroutine inherits Coroutine {
#strict
(s Self) -> Dynamic {
yield x
}
x Int64
}
coro = MyCoroutine()
coro.x = 12
coro_f = coro.future
loop AsyncLoop()
loop.run(coro)
while not coro_f.has_value.atomic_load(volatile: true) do
Thread.yield_time()
end
print(coro_f.value) #! prints 12
loop.shutdown()
Note
In the above code a new AsyncLoop is created since the code is in the global scope. However if defined within another coroutine it would be best to reuse the loop from that coroutine instead of starting a new one!
Notice the use of yield
to return values from a coroutine. This make
coroutines generator objects which are discussed in another section. For
example if a coroutine is not finished running yet and perhaps is waiting on
the network a CoroutineNoValue
object must be yielded to relinquish the
execution time to other coroutines and reschedule this one for another time to
recheck.
Async/Await syntax
Gambol provides syntax sugar to simplify the process of creation and running of
coroutines. The async
keyword will make the creation of coroutine objects as
simple as function definitions and the await
keyword will take care of
registering the coroutine with the appropriate loop and waiting for its value
to arrive. Therefore the above code is roughly equivalent to this:
import async
async fun my_coro(x Int64) -> Int64 { yield x }
print(await!(my_coro(12))) #! prints 12 (eventually!)
If you'd like to call the same coroutine multiple times or for other reasons, you can await a coroutine in your own loop.
import async
async fun my_coro(x Int64) -> Int64 { yield x }
loop = async.AsyncLoop()
coro = my_coro(12)
print(await!(coro) in loop) #! prints 12 (eventually!)
Generic Functions
You can parameterize functions and types at compile time. The parameters can be types, function interfaces or expressions with compile time evaluation e.g. functions or operations on literals and such. The constant expressions must all come after the type parameters and be preceeded with a semicolon:
fun my_fun[T, U; c](a T, b U) -> T, U {
return c * a, c * b
}
r1, r2 = my_fun[Int16, Int128; 10](3, 5)
print(r1) #! prints 30
print(r2) #! prints 50
The function call above instantiates or composes a new function from the
generic template using the parameters and constant expressions specified. As a
result r1
above will take the type Int16
and r2 will take on the type
Int128
.
Type inference
When type parameters are used in function parameters, Gambol can infer the type from the arguments passed to the function and in most cases explicitly writing them down is not necessary. If the arguments contain Dynamic or unpack operations however they'll need to be evaluated at runtime and therefore type inference won't work.
fun my_fun[T](a T, b T) -> T {
return a * b
}
print(my_fun(2,3)) #! prints 6
Tip
If you are designing a library with parameteric types it's best to explicitly write down the type parameters to cover for cases where the parameters might be Dynamic.
Automatic parameters
There is another syntax for writing generics without naming the type parameters
using the keyword auto
. This snippet is equivalent to the above:
fun my_fun(a auto, b auto) { a * b }
print(my_fun(2,3)) #! prints 6
The keyword auto
can also be omitted completely in writing parametric
functions but it provides the opportunity to add more information to the
parameter's type specifier such as it being by-ref or nillable.
fun my_fun(a, b) { a * b }
print(my_fun(2,3)) #! prints 6
You can also mix and match parameters that are explicitly typed, use the auto
keyword or have no type at all.
Variadic type parameters and constant expressions
Generic types and function can also accept variadic type parameters and variadic constant expressions. This is mostly useful in writing abstract types and libraries but could have other uses as well.
fun my_fun[*T](params T) {
#unroll
for i in ..len!(params) {
print(`param ` i `: ` params[i])
}
}
my_fun[NFloat, String, Int64?, Float](3.14, `hello`, , +Inf)
output
param 0: 3.14000000000000012434
param 1: hello
param 2: Nil
param 3: +Inf
Here is another example for constant expressions:
fun call_all[*T; *f](params T) {
#unroll
for i in ..len!(f) { f[i](params) }
}
fun f1(a Float, b String) { print(`f1 a: ` a `, b: ` b) }
fun f2(a Float, b String) { print(`f2 a: ` a `, b: ` b) }
fun f3(a Float, b String) { print(`f3 a: ` a `, b: ` b) }
call_all[NFloat, String; f1, f2, f3](3.14, `hello`)
output
f1 a: 3.14000000000000012434, b: hello
f2 a: 3.14000000000000012434, b: hello
f3 a: 3.14000000000000012434, b: hello
Attributes
Here is the list of attributes that functions interpret as meaningful.
#extern
These functions will have a c calling convention and are callable from other applications. Function interfaces marked extern also take on the C calling convention. C convention also means that all dynamic features are disabled. You cannot pass Dynamic variables or unpacks to the function. You also cannot take or return structs and in those cases you'd have to use pointers.#default
Some methods such as assignment overload=
have a default implementation in the compiler. Adding this attribute means you won't provide the function body and let the compiler implement the function.#strict
Methods with this attribute will have a strict interface and enforce a strict interface also on all derived methods. This is the default (and only) inheritance behavior in C++ and most members of the C family of languages.#Gambol.function.alwaysinline
The function will be inlined in every module it is used in#Gambol.function.localinline
The function will be inlined only in the module it is defined in.#Gambol.function.alwaysimplement
The function will be reimplemented in every module it is used in to let the backend decide if it wants to inline it or not.
LLVM attributes
The following attributes mirror their counterparts in LLVM. They are all documented under LLVM's Language Reference.
#Gambol.function.cold
#Gambol.function.hot
#Gambol.function.inlinehint
#Gambol.function.nofree
#Gambol.function.noinline
#Gambol.function.nomerge
#Gambol.function.noreturn
#Gambol.function.norecurse
#Gambol.function.willreturn
#Gambol.function.nosync
#Gambol.function.nounwind
#Gambol.function.optnone
#Gambol.function.optsize
#Gambol.function.readnone
#Gambol.function.readonly
#Gambol.function.writeonly
#Gambol.function.uwtable_sync
#Gambol.function.uwtable_async
#Gambol.function.speculative_load_hardening
#Gambol.function.speculatable
#Gambol.function.ssp
#Gambol.function.sspstrong
#Gambol.function.sspreq
#Gambol.function.nocf_check
#Gambol.function.mustprogress