Functions
Function expressions define functions. Functions are defined using the following syntax.
def [name] ( [params] ) [locals] => <expression>
A function's name is optional. If left out, the function is an anonymous function. The function's parameter list may contain zero or more parameters spearated by commas. As Lavender is untyped, the parameters are given names only. Some examples of function definitions below.
def Inf() => 1 / 0
def BoolVals() => { True, False, FileNotFound }
def log2(x) => log(x) / log(2)
def isTrue(x) => x = true
Fixing and Asscoiativity
If a function takes parameters, it may be either prefix or infix. When calling a function, prefix functions are always placed before their parameters, while infix functions are placed after their first parameter. To make a function infix, start its name with i_. Functions are prefix by default, but this can be specified explicitly by using the prefix u_. These prefixes are not part of the function name.
> def plus(x, y) => x + y ' Same as: def u_plus(x, y) => x + y repl:plus > def i_minus(x, y) => x - y repl:minus > plus(3, 2) 5.0 > 5 minus 3 2.0Note that infix functions must be placed directly after their first parameter. Prefix and infix functions may also be overloaded (
func and i_func are different functions), but this practice is discouraged.
Infix functions additionally may be either left or right associative. For example, multiplication is left associative while exponentiation is right associative.
2 * 3 * 4 = (2 * 3) * 4
2 ** 3 ** 4 = 2 ** (3 ** 4)
To make a function right associative, prefix the name with r_. Since only infix functions can be right associative, i_ does not need to be specified for right associative functions.
Precedence
Infix functions have a precedence based on the first character of their names. This ordering is similar to the ordering used in the Scala language.
Precedence from greatest to least? ~*** / %+ -< >= !&^|All letters and _
Function Values
A function value is made by prefixing a function name (either qualified or unqualified) with the backslash character. Infix functions additionally are postfixed by the backslash character. Function values denote the function itself, which enables functions to be treated as values.
> def f(x, y) => x + y
repl:f
> \f
repl:f
> def g() => 42
42.0
> \g
42.0
Functions that take no parameters must evaluate to a constant value, thus they are treated by the language as values themselves. A consequence of this is that for all functions that take no parameters, their function value is equal to their result.
Nested Functions
Functions may be nested in other expressions. In addition to their own parameters, nested functions capture any parameters declared in enclosing functions. Functions defined inside other functions are only accessible within the outer function, whereas "top-level" functions may be accessed anywhere.
> def f1(x) => x + 1 ' Define function f1 repl:f1 > f1(12) ' Call f1 13.0 > def f2(x) => (def inner(i) => x + i) ' Define f2 with a nested function inner repl:f2 > f2(2) repl:f2:inner[2.0] ' Returns the inner function > inner(3) Unqualified name not found: inner ' Nested function not accessible
Calling Values as Functions
Sometimes it may be necessary to use a value as a function. Often the value in question is the result of another function, or a parameter to a function. Square bracket notation allows one to use values as functions.
> def f(x) => (def(i) => x + i) repl:f > f(1) ' Returns the nested function repl:f:[1.0] > [f(1)](2) ' Square bracket notation 3.0Some real-world examples are the standard library map and reduce functions. These functions are known as forwarding functions. They exist only to forward their arguments. def i_map(obj, func) => [[obj](\map\)](func)
def i_reduce(obj, identity, func) => [[obj](\reduce\)](identity, func)
In newer versions of Lavender, you can also use parenthesis notation to call values as functions. Rewritten to use parenthesis notation, the above example would look like this.
> f(1)(2)
3.0
Parenthesis notation is generally preferred to square bracket notation, as it looks more like a regular function call. However, there are two critical differences between parenthesis notation and function calls. First, the parentheses in parenthetical notation are required, whereas they are optional for regular function calls. Second, prefix functions have precedence over parenthetical notation. This allows for natural interop with alphanumeric functions, but may be unintuitive for symbolic functions.
> def f(x) => "hello"(x)
repl:f
> !f(1) = !("hello"(1))
1.0
> !"hello"(1) = (!("hello"))(1)
1.0
> f(1)(0) = (f(1))(0)
1.0
Argument Passing Conventions
In Lavender, arguments to functions may be passed by value, or passed by name. Pass-by-value evaluates the argument once before calling the function, while pass-by-name evaluates the argument each time it is used by the function. Passing conventions are defined by the function. By default, arguments are passed by value, but pass-by-name may be specified by using the => token before the parameter name.
def i_if(=> expr, cond)
def i_&&(a, => b)
When calling a value as a function, the passing conventions of the underlying function are respected.
Varargs Functions
def Vect(...a) => a ' Make a vector
Functions may be defined as taking a variable number of arguments by adding the ellipsis token (...) before the last parameter. The extra arguments are boxed into a Lavender vect. Value calling syntax respects varargs functions. Note that passing a single vect as the varargs parameter will still result in boxing.
Piecewise Functions
When defining a function, it may either have one body or more than one body. Functions with more than one body are called piecewise functions. Piecewise functions are written as a sequence of function bodies, each composed of a result expression and a condition expression separated by a semicolon.
(def sgn(x) ' The mathematical sign function
=> 1 ; x > 0
=> 0 ; x = 0
=> -1 ; x < 0
)
The first body, as given in the source code, for which the condition expression yields a truthy value is executed. However, the runtime may choose to evaluate conditions in a different order than given in the source code. If no condition is satisfied, the function returns the value sys:undefined.
> (def fact(n) ' Factorial function > => 1 ; n = 0 > => n * fact(n - 1) ; n > 0 > ) repl:fact > fact(0) 1.0 > fact(3) 6.0 > fact(-2) <undefined>When defining nested functions, piecewise function bodies bind to the innermost function, unless the inner function is explicitly closed with parentheses.
def f(x) => def(y) => y ; x > y ' Inner function is piecewise def f(x) => (def(y) => y ; x > y) ' Inner function is piecewise def f(x) => (def(y) => y) ; x > y ' Parse error. 'y' is not declared