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
.
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 Function
s 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
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.
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
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.
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
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.
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
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"]