A Function value is a value like any other: It can be passed around like an argument, returned, stored, and so on. The most common way of creating a new Function is with define.

Definitions

All Function values created through define cannot be changed. The simplest definition is a Function that does not take any input or return any value.

define hello
{
    print("Hello!") # Hello!
}

var h: Function() = hello

h()

A Function that does not specify a return value actually returns unit of type Unit behind the scenes. Doing so allows Functions that do and don't return values to be treated equally by methods such as List.each.

Now for a more useful function.

define add(left: Integer, right: Integer): Integer
{
    return left + right
}

var a: Function(Integer, Integer => Integer) = add

print(a(1, 2)) # 3

In the first example, parentheses are omitted because no arguments are taken. Similarly, the colon (which comes before the return type) is omitted because no value is explicitly returned.

It is a syntax error to have empty parentheses in any definition, or to have a colon if there is no return type.

Using a function as an argument can be done as follows.

define square(input: Integer): Integer
{
    return input * input
}

define apply_action(a: Integer, fn: Function(Integer => Integer)): Integer
{
    return fn(a)
}

print(apply_action(10, square)) # 100

Lambdas

In some cases, a Function is necessary but creating a permanant definition is unnecessary. In such cases, a lambda can be used.

The return value of a lambda is the last expression inside of it. If a lambda finishes with a statement (such as an if block), it will return unit like the noop above.

var numbers = [2, 4, 6]

numbers = numbers.map((|a| a * a))

print(numbers) # [4, 16, 36]

In the above example, the lambda is the first argument to a function. In such cases, the opening and closing parentheses can be omitted.

var numbers = [1, 2, 3]

numbers = numbers.map(|a| a * a)

print(numbers) # [1, 4, 9]

It is possible to create a lambda that does not take arguments.

var hello: Function(=> String) = (|| "Hello" )

print(hello()) # Hello

Lambda arguments allow type inference.

var math_ops = ["+" => (|a: Integer, b: Integer| a + b),
                "-" => (|a, b| a - b)]


print(math_ops["+"](1, 2)) # 3

Lambdas do not support keyword arguments, optional arguments, or variable arguments.

Closures

The above declarations only use global variables and arguments provided. A closure is a Function that uses variables in an upward scope (upvalues).

Any definition (including class methods) can be a closure.

define get_counter: Function( => Integer)
{
    var counter = 0

    define counter_fn: Integer
    {
        counter += 1
        return counter
    }

    return counter_fn
}

var c = get_counter()
var results = [c(), c(), c()]

print(results) # [1, 2, 3]

Lambdas can also be closures.

define list_total(l: Integer...): Integer
{
    var total = 0

    l.each(|e| total += e )

    return total
}

print(list_total(1, 2, 3)) # 6

Class constructors and methods can be closures too. Class methods cannot close over self, and neither can close over parameters to a class constructor.

enum Status
{
    Fail,
    Pass,
    Skip
}

class Totals(input: Status...)
{
    public var @fail_count = 0
    public var @pass_count = 0
    public var @skip_count = 0

    {
        var fail = 0
        var pass = 0
        var skip = 0

        input.each(|e|
            match e: {
                case Fail:
                    fail += 1
                case Pass:
                    pass += 1
                case Skip:
                    skip += 1
            }
        )

        @fail_count = fail
        @pass_count = pass
        @skip_count = skip
    }
}

var t = Totals(Fail,
               Pass, Pass, Pass,
               Skip, Skip)

print(t.fail_count) # 1
print(t.pass_count) # 3
print(t.skip_count) # 2

Features

Functions in Lily have a number of different features available to them. All function kinds except for lambdas can make use of all of these features.

Forward

Occasionally, two definitions rely on each other. In such cases, forward allows creating a definition that will be resolved later.

When there are one or more unresolved forward definitions, both variable declaration and import are blocked.

It is a syntax error to have unresolved definitions at the end of a scope.

forward define second(Integer, Integer): Integer { ... }

define first(x: Integer, total: Integer): Integer
{
    if x != 0: {
        x -= 1
        total = second(x, total * 2)
    }

    return total
}

define second(x: Integer, total: Integer): Integer
{
    if x != 0: {
        x -= 1
        total = first(x, total * 2)
    }

    return total
}

print(first(4, 2)) # 32

Class methods can be forward declared as well. Similar to forward definitions, class properties cannot be declared while there are unresolved methods.

class Example
{
    forward private define second(Integer, Integer): Integer { ... }

    # Declaring properties here is not allowed.

    public define first(x: Integer, total: Integer): Integer
    {
        if x != 0: {
            x -= 1
            total = second(x, total * 2)
        }

        return total
    }

    private define second(x: Integer, total: Integer): Integer
    {
        if x != 0: {
            x -= 1
            total = first(x, total * 2)
        }

        return total
    }
}

print(Example().first(4, 2)) # 32

Varargs

A definition can be allowed to take an arbitrary number of values, with ....

define sum(numbers: Integer...): Integer
{
    var total = 0
    numbers.each(|e| total += e )
    return total
}

var s: Function(Integer... => Integer) = sum

print(s())        # 0
print(s(1, 2, 3)) # 6

Class constructors, class methods, enum methods, and variants all allow for variable arguments.

Optargs

A definition can specify optional values with *<type>=<value>. The value given can be simple, or an expression. Required arguments cannot occur after an optional argument.

define optarg(a: *Integer = 10): Integer
{
    return a + 10
}

var o: Function(*Integer => Integer) = optarg

print(optarg(100)) # 110
print(o())         # 20

Optional arguments are evaluated in the callee's scope.

define return_a(a: *String=__function__): String
{
    return a
}

var r: Function(*String) = return_a

print(r())       # return_a
print(r("asdf")) # asdf

Additionally, optional arguments are evaluated each time they are seen.

define modify(v: Integer,
              a: *List[Integer]=[1, 2, 3])
              : List[Integer]
{
    a.push(v)
    return a
}

var m: Function(Integer, *List[Integer] => List[Integer]) = modify

print(m(4))  # [1, 2, 3, 4]
print(m(10)) # [1, 2, 3, 10]
print(m(70)) # [1, 2, 3, 70]

Optional arguments are evaluated from left to right. Arguments on the right can depend on arguments to their left.

define my_slice(source: List[Integer],
                start: *Integer = 0,
                end: *Integer = source.size()): List[Integer]
{
    return source.slice(start, end)
}

print(my_slice([1, 2, 3], 1))    # [2, 3]
print(my_slice([4, 5, 6], 0, 1)) # [4]

Variable and optional arguments can be mixed. By default, the vararg parameter receives an empty List if no values are passed. Mixing these two features allows a different default value:

define optarg_sum(a: Integer,
                  b: *Integer = 10,
                  args: *Integer... = [20, 30]): Integer
{
    var total = a + b

    for i in 0...args.size() - 1: {
        total += args[i]
    }

    return total
}

var opt_sum: Function(Integer,
                      *Integer,
                      *Integer...
                      => Integer) = optarg_sum

print(opt_sum(5))              # 65
print(opt_sum(5, 20))          # 75
print(opt_sum(10, 20, 30, 40)) # 100

Keyargs

A definition can specify keyword arguments by using :<keyword> <name>: <type>. Keyword arguments can be used give clarity when multiple arguments of a particular type are passed. Keyword arguments can also be passed in a custom order, unlike position arguments.

define simple_keyarg(:first  x: Integer,
                     :second y: Integer,
                     :third  z: Integer): List[Integer]
{
    return [x, y, z]
}

print(simple_keyarg(1, 2, 3))                         # [1, 2, 3]
print(simple_keyarg(1, :second 2, :third 3))          # [1, 2, 3]
print(simple_keyarg(:third 30, :first 10, :second 5)) # [10, 5, 30]

It isn't necessary to name all arguments:

define tail_keyarg(x: Integer, :y y: Integer): Integer
{
    return x + y
}

print(tail_keyarg(10, 20))    # 30
print(tail_keyarg(10, :y 20)) # 30

Calling a function with keyword arguments has some restrictions.

define simple_keyarg(:first  x: Integer,
                     :second y: Integer,
                     :third  z: Integer): List[Integer]
{
    return [x, y, z]
}

# SyntaxError: Positional argument after keyword argument.
# simple_keyarg(:first 1, 2, 3)

# SyntaxError: Duplicate value provided to the first argument.
# simple_keyarg(1, :first 1, 2, 3)

Keyword arguments are evaluated and contribute to type inference in the order that they're provided.

var keyorder_list: List[Integer] = []

define keyorder_bump(value: Integer): Integer
{
    keyorder_list.push(value)
    return value
}

define keyorder_check(:first  x: Integer,
                      :second y: Integer,
                      :third  z: Integer): List[Integer]
{
    return keyorder_list
}

var check = keyorder_check(:second keyorder_bump(2),
                           :first  keyorder_bump(1),
                           :third  keyorder_bump(3))

print(check) # [2, 1, 3]

Finally, different argument styles can be mixed.

define optkey(:x x: *Integer = 10,
              :y y: *Integer = 20): Integer
{
    return x + y
}

print(optkey())        # 30
print(optkey(50, 60))  # 110
print(optkey(:y 170))  # 180
print(optkey(4, :y 7)) # 11

define varkey(:format fmt: String,
              :arg args: *String...=["a", "b", "c"]): List[String]
{
    args.unshift(fmt)
    return args
}

print(varkey("fmt"))                # ["fmt", "a", "b", "c"]
print(varkey("fmt", "1", :arg "2")) # ["fmt", "1", "2"]