Skip to content

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