View on GitHub

Lavender

An experimental pure functional programming language.

Functions

Functions are the basic building block of Lavender programs. In fact, a Lavender program is nothing more than the evaluation of functions, either built in or user-defined.

Functions are defined using function expressions, which begin with the keyword def. A given function expression lasts until the next unnested comma or closing grouper.

Function Names

Functions may optionally be named. Functions may have alphanumeric or symbolic names. The type of name has no effect on how the function may be used.

Examples:

def func(a) => a
def *(a) => a
def(a) => a

Fixing and Associativity

Function fixing and associativity is specified by adding a prefix to the function name. Left infix functions use the i_ prefix, and right associative infix functions use the r_ prefix. Regular prefix functions may optionally have the u_ prefix. These prefixes are not part of the function name.

def u_prefix(a) => a
def i_leftInfix(a, b) => a
def r_rightInfix(a, b) => a
def i_postfix(a) => a

Note: When right and left associative infix functions are mixed, right associative functions are a half level of precedence higher than left infix functions of otherwise equal precedence.

If a left infix function takes only one parameter, it is a postfix function. Right infix functions cannot be postfix.

Namespaces and Overloading

If a function is defined at top-level (not inside another function), the user may refer to it by its simple name, or by its qualified name. A function’s qualified name is the name of its namespace, followed by a colon, followed by the simple name.

' --- qual.lv ---
def func(a) => a

' --- REPL ---
@import qual
qual:func(3)

Prefix and infix functions within a namespace may have the same name. An example from the standard library is the - function. This is not recommended in general, however.

def -(a) => negation
def i_-(a, b) => subtraction

Parameters and Locals

Functions may take zero or more parameters. Parameters may be either normal parameters or by-name parameters. When calling a function that takes parameters by-name, the arguments corresponding to these parameters are implicitly by-name.

def f(a, b) => ...  ' Binary function
def g(a, => b, c) => ...  ' Ternary function whose second parameter is by-name
g(1 + 2, 3 + 4, 5 + 6)  ' `3 + 4` is implicitly by-name

Varargs

The last parameter to a function may optionally be a varargs parameter, regardless of its by-name status. When calling such a function, any number of arguments (including zero) may be passed that correspond to this parameter. The arguments are wrapped in a vect when passed to the function. If the varargs parameter is by-name, each corresponding argument is implicitly by-name.

def f(a, ...b) => ...
f(1)  ' b is {}
f(1, 2)  ' b is { 2 }
f(1, 2, 3)  ' b is { 2, 3 }

Function Locals

Functions may define a list of local values using the let keyword. These values are initialized with an arbitrary expression, then are accessible to the remainder of the function. Function local values may access parameters and locals defined previously, but may not refer to future locals nor themselves. A function defined in a local value captures only the locals defined before it.

def f(a)
    let b(a), c(b + 1) => ...
def f(a)
    let b(c), c(b) => ...  ' error: b cannot refer to c
def f(a)
    let b(a + 1), c(def(d) => ...) => ...  ' c captures a and b only

Nested Functions

Functions may be defined within other functions. Such a function is a nested function. Nested functions capture values bound in the enclosing function(s), which refer to the enclosing function’s arguments, as expected. Nested function definitions bind as far as possible, and so should be enclosed in groupers if they appear in the middle of the outer function definition.

def f(a) => def g(b) => { a, b }
f(1)  ' --> repl:f:g[1] (g captures value `1`)
f(1)(2)  ' --> { 1, 2 }

Piecewise Functions

Functions may be defined piecewise, where different function bodies are evaluated under certain conditions. Each body begins with the arrow token => and the condition is separated from the body using the semicolon ;. The body corresponding to the first condition that is truthy is evaluated as the value of the function.

(def f(a)
    => a + 1 ; a > 0
    => a - 1 ; a < 0
    => 42 ; a = 0
)

Calling Values

Lavender statically checks written function calls for correct arity. However, you can also call arbitrary values, which Lavender checks for validity at runtime. These arbitrary values may be literals, names, or any expression.

def f(a) => a(2, 3)
"hello world"(4)  ' --> o
f(\+\)   ' --> 5
f(\+)    ' --> <undefined>

Calling values uses the same syntax as calling functions. Notably, parentheses may be omitted around a single argument.

Warning: Using overloaded functions in value calls without parentheses may produce ambiguous expressions. In these cases, the expression is parsed as a non-value call. For example, the expression a - 2 is parsed as subtracting 2from a, not as calling a with the value -2, if a is a value.