Functions

Functions are the most essential building block of Fuse Since you can create anything even numbers using functions alone. As mentioned earlier in the introduction, We treat functions just like every other value; They can be assigned to variables, Passed to functions as arguments, or returned from another function.

There any many different ways to describe a function in Fuse, But whether it is a local function, a function exported from another module, a struct or trait method, or even a function imported from a Lua library they are all first-class types and will follow the same rules.

Function Declaration

In Fuse we can declare a function using either function or its shorter version fn keywords.

function fun()
  print("functions are fun!")
end
-- or
fn fun()
  print("functions are fun!")
end

A function can return exactly one value. Functions without a return statement will implicitly return their last expression value. If a return type isn’t provided the compiler will try to infer the return type from the first returning branch of function otherwise will assume the return type to be Unit(()).

fn fun()
  print("functions are fun!")
end
-- or be explicit
fn fun() -> ()
  print("functions are fun!")
  return ()
end

fn fun() -> string
  "functions are fun!"
end

Fuse supports the concept of tuples which can be used in place of multiple return values. By doing so in addition to having a more concrete type signature for these functions we also get to keep all return values in one place instead of immediately breaking them into the individual return values.

fn fun()
  ("value", 42, true)
end
-- same as
fn fun() -> (string, number, boolean)
  return ("value", 42, true)
end
-------------------------------------
let result = fun()
let (str, num, bool) = result
-- or assign them directly
let (str, num, bool) = fun()

If we don’t want the last expression to be returned, We should either return explicitly or annotate our function with Unit return type.

-- so either we return `()` explicitly
fn fun()
  ("value", 42, true)
  return ()
end
-- or annotate the function as such
fn fun() -> ()
  ("value", 42, true)
end

Note: Unit(()) is just a tuple with no values, Since any tuple without a value is equal to any other empty tuple therefore at any time there can only exist one of such tuples. This can also explain the reason behind the syntax of Unit.

Tuples that are immediately expanded will optimize away in runtimes with support for multiple return values. Learn more about Tuples.

Functions with only a single line of body can be expressed using the following syntax.

fn fun() => print("functions are fun!")

Function Usage

In Fuse functions are called using parantecies, Unlike Lua we do not allow omitting these parantecies.

get_user()

A method can be accessed using a dot(.) notation.

get_user().username()

Parameters

A function can accept zero, one, or many parameters.

fn sum(a: number, b: number) -> number => a + b

assert_eq(sum(10, 20), 30)

Function parameters can have a default value which means they are not required to be passed in. The default value must be compile-time constant.

fn lerp(v0: number, v1: number, t: number = 1) -> number
  return v0 + t * (v1 - v0)
end

assert_eq(lerp(0, 100), 100)
assert_eq(lerp(0, 100, 0.5), 50)

We can also pass arguments using their names, It can be extremely useful when we have a lot of nil and boolean arguments which can make it hard to read.

fn configure(
  backend: Backend,
  enable: boolean,
  service: Service | nil,
  api_key: string | nil,
  policies: Policy[] | nil = nil,
  formatter: Formatter | nil = nil,
  options: AdditionalOptions | nil = nil
  development_mode: boolean = false,
  serialization_type: SerializationType = SerializationType.Binary)
  -- function magic happens here!
end

-- ...

-- calling without named parameters
configure(my_backend, true, my_service, nil, nil, nil, nil, false, SerializationType.Json)
-- calling with named parameters
configure(
  my_backend,
  enable: true,
  my_service,
  api_key: nil,
  serialization_type: SerializationType.Json)

Note: While it is not necessary to name all arguments, after omitting the first parameter with default value you have to name all subsequent arguments.

If a parameter with a default value proceeds a parameter with no default, The only way to use the default value is to call the function with named arguments.

fn greeting(greet: string = "Hello", name: string)
  print("${greet}, ${name}!")
end

greeting(name: "Sam")

Closures

In Fuse, closures are defined using the exact syntax for functions but omitting the name.

let closure = fn(a: number, b: number) -> number => a + b

While it may look like an anonymous function in other languages it is in fact a true closure that can capture values from its scope and also be inlined directly in the call site.

fn lcg(seed: number)
  let a = 1140671485
  let c = 128201163
  let m = 2 ^ 24

  let mut rand = seed
  return fn()
    rand = (a * rand + c) % m
    return rand
  end
end

let random = lcg(1)

assert_eq(random(), 10581448)
assert_eq(random(), 11595892)
assert_eq(random(), 1323120)
assert_eq(random(), 16081019)

Function Type Expression

As we have read earlier, Fuse has first-class support for functions that’s why we should be able to talk about the type of function in our code without any extra effort. We can annotate the type of a function with the exact syntax used for creating it.

fn fun(a: number, b: number) -> number => a + b

let my_fun: fn(number, number) -> number = fun

Since now we have a way of expressing types of functions it is possible to have them as a function parameter or its return type. This notion of higher-order functions enables us to create more declarative programs.

Here’s a possible implementation of the filter function for numbers.

fn filter(nums: number[], predicate: fn(number) -> boolean) -> number[]
  let result: number[] = []

  for num in nums do
    if predicate(num) then
      result.insert(num)
    end
  end

  return result
end

Inline

Using higher-order functions imposes some performance penalties, Each function needs to capture values accessed in its body and store them along with the closure which will generate excess garbage and increase the memory usage of our program. It also means that the CPU has to do more jumping around to call our function and return its value to the calling site.

In many situations we can prevent this overhead by inlining our functions, It can be done by marking the function with the inline attribute.

#[inline]
fn compute(num: number) -> number
  return num ^ 2
end

for i in 0..100_000_000 do
  print(compute(i))
end

It will result in a code similar to if it was written inside of the loop.

for i in 0..100_000_000 do
  print(i ^ 2)
end

Inlining a function will grow the size of our Lua code but it will pay off by improved execution speed especially when used in loops.

We can also use inline for function parameters that are function-type expressions.

let execution_time = get_execution_time()

fn execute(#[inline] func: fn() -> boolean)
  let time = get_time()
  if func(time) then
    print("Function has been executed")
  end
end

execute(fn(time) => if time > execution_time then true else false end)

The inline attribute will hint to the compiler that we prefer this function get inlined, But in some situations, the compiler may fail to do so for example in the example below it isn’t possible to to inline the passed closure to the execute function since it captures is_admin which is a local variable to the run function.

let execution_time = get_execution_time()

fn execute(#[inline] func: fn() -> boolean)
  let time = get_time()
  if func(time) then
    print("Function has been executed")
  end
end

fn run()
  let is_admin = get_user().role == Role::Admin
  execute(fn(time) => if is_admin and time > execution_time then true else false end)
end

run()

If the compiler tries to inline the func closure it will result in a transformed code like this:

let execution_time = get_execution_time()

fn execute()
  let time = get_time()
  if is_admin and time > execution_time then -- notice is_admin dosn't exists here!
    print("Function has been executed")
  end
end

fn run()
  let is_admin = get_user().role == Role::Admin
  execute()
end

run()

Since we can not access the captured value is_admin in the execution site of our func function, the Compiler will fail to inline this closure and will fall back to using a normal closure. We can solve this by inlining the whole execute function instead of just the closure parameter used by it.

let execution_time = get_execution_time()

#[inline]
fn execute(func: fn() -> boolean)
  let time = get_time()
  if func(time) then
    print("Function has been executed")
  end
end

fn run()
  let is_admin = get_user().role == Role::Admin
  execute(fn(time) => if is_admin and time > execution_time then true else false end)
end

run()

Now that the compiler will inline the execute function we have access to all of our captured values inside our closure and it will result in a code as if we have written this instead:

let execution_time = get_execution_time()

fn run()
  let is_admin = get_user().role == Role::Admin
  let time = get_time()
  if is_admin and time > execution_time then
    print("Function has been executed")
  end
end

run()

Note: Depending on the code structure the compiler may decide to inline the execute function even if it is not marked as inline. In this example, we assume that there is no optimization happening by the compiler which isn’t always the case.

No Inline(#[inline(never)])

By default all closures passed to an inline function will also get inlined, This behavior can be prevented by explicitly marking the parameter with the inline(never) attribute.

#[inline]
fn func(a: () -> boolean, #[inline(never)] b: () -> number)
  -- ...
end

Note: The inline attribute does not guarantee the function being inlined, It will hint to the compiler that it should either try to inline the function in case of #[inline] or it should prevent it from being inlined in case of #[inline(never)]. When a function hasn’t been marked explicitly the compiler will decide whether it should be kept intact or get inlined at the call site.