langur

functions

Functions in langur are pure in that they are not allowed to set values outside of their boundaries. They could still have "side effects," such as using writeln().

Functions are first-order. Both user-defined functions and built-in functions can be passed around as values.

The f token designates a function definition.

Functions may be defined with explicit parameters, no parameters, or implied parameters, or as operator implied functions, as shown in the examples below.

f(.x, .y) { .x + .y } # 2 explicit parameters f(.y) { .x + .y } # 1 explicit parameter and closure on 1 value f() { .x + .y } # no parameters (closure on 2 values) f { .x + .y } # 2 implied parameters f{+} # operator implied function f{x 3} # nil left partially implied function

Curly braces are not required for a function declaration if the function body is a single statement (except for implied functions). The example that follows also uses implied parameters.

f .x + .y

implied parameters

Parameters are implied by having no parentheses directly attached to the f token. For example, f (.x - '0') ^ (.x - '0') is a function that has no explicit parameters (1 implied).

The implied order of implied parameters is based on the string sort order of their names, not on their order of appearance in the function.

Implied parameters are convenient for small functions, such as in the following example.

val .factorial = f if(.x < 2: 1; .x x self(.x - 1))

closures

Functions defined to use values outside their boundaries are closures. After a closure is defined, it is not affected by changes to those outside values.

Closures cannot be combined with implied parameters.

operator implied functions

Operator implied functions are defined without parentheses or spacing, using curly braces with an infix operator, such as f{x}. This is the same as writing f .x x .y.

nil left partially implied functions

These are very similar to operator implied functions, but include a value on the right, such as f{x 3} to multiply a passed value by 3 (likely used with map()).

parameter mutability

By default, function parameters are immutable.

To make a set of parameters mutable, precede the parameter names with a var token within the parameter list. To make the rest of the parameters immutable, precede the names with a val token.

f(.x, .y) ... # .x and .y immutable

f(var .x, .y) ... # .x and .y mutable

f(var .x, val .y) ... # .x mutable and .y immutable

recursion

Recursion can be used when the function definition is directly assigned to a variable, such as the following.

val .fibonacci = f(.x) if .x < 2 { .x } else { .fibonacci(.x - 1) + .fibonacci(.x - 2) }

This example function uses recursion and a single statement (if expression) without curly braces (braces shown are part of the if expression, not for the function).

Recursion can also be used by making a call with the self token.

val .fibonacci = f if .x < 2 { .x } else { self(.x - 1) + self(.x - 2) } # or shortened to... val .fibonacci = f if(.x < 2: .x ; self(.x - 1) + self(.x - 2))

Using the self token, recursion does not require assignment (may be used with anonymous functions). This also makes it possible to combine implied parameters and recursion, which you cannot do when using the name.

Note that the built-in map(), fold(), and foldfrom() functions, or loops, can often be used instead of recursion.

return value

An explicit return can be used, but a function's last value is its implicit return value.

All functions return something, even if it's nothing (null).

function calls

User-defined functions can be called with parentheses, such as .x().

Built-in functions can be called with either parentheses or an unbounded argument list, such as split re/[!_]/, "a!b_c" which would split the string "a!b_c" into an array of 3 strings.

If you do not use an unbounded list or parentheses on a function name, it is not a call, but a reference to the function.

argument expansion

As of 0.8.7, you can use a ... operator on the last argument passed to a function, to expand an array argument into multiple arguments.

zip(X([7], [14, 21])...) # result == [7, 7, 14, 21]

val .x = [[21, 7], [42, 14], [96, 21]] zip(.x...) # result == [21, 42, 96, 7, 14, 21]

parameter expansion

As of 0.8.11, you can use the ... operator for parameter expansion. This allows a user-defined function to receive a variable number of arguments. The last arguments of the function will be placed into a single array. This also allows for setting limits for this expansion (with a default range of 0 to unlimited). -1 indicates unlimited for this purpose. See the following examples.

val .a = f(... .x) { } # accepts 0 or more arguments val .b = f(...[1..9] .x) { } # accepts 1 to 9 arguments val .c = f(.x, ...[4] .y) { } # accepts 5 arguments val .d = f(.x, ...[2 .. -1] .y) { } # accepts 3 or more arguments val .e = f(.x, ...[2 .. 6] .y) { } # accepts 3 to 7 arguments

unbounded lists

Unbounded lists can often be used to call built-in functions. They end at a line return or end of file, or closing parenthesis, curly brace, or square bracket. So, they are not bounded by their own set of parentheses.

val .n = random 10 # random() parameter list bounded by line return or end of file # same as val .n = random(10)

val .factorial = f(.n) fold(f{x}, series .n) # series() parameter list bounded by closing parenthesis of fold()

The following example contains no unbounded lists. It merely allows continuation of a list after a line return preceded by a comma.

val .new = foldfrom( f(.hash, .key, .value) .hash ~ h{.key: .value}, h{}, w/a b c d/, [1, 2, 3, 4], ) # use toHash() instead, BTW

Unbounded lists end before a comma that precedes a line return.

# with .s already defined ... val .nums = map( f(.i, .d) if(.i div 2: .d x 3; .d), series 13, map f .c-'0', s2cp .s, ) # unbounded list on series() ends before a comma preceding a line return # also note that both lists for s2cp() and the second call to map() end before a comma preceding a line return

An unbounded list takes all items it until it is closed. If an unbounded list is used within another, it takes the remaining items.

writeln "filtered: ", filter f .x div 2, .arr # unbounded lists used with both filter() and writeln(); filter() takes last arguments (an anonymous function and a variable)

val .ns = toString random 10 # generate a random number from 1 to 10 and save as a string to .ns # same as val .ns = toString(random(10))

An unbounded list cannot be used in the test expression of an if expression (must use parentheses for a function call in this case).

writing for clarity

Sometimes using parentheses and explicit parameters will give more clarity. For example, I could write...
val .factorial = f fold f .a x .b, series .n

...but this might be more clearly written as...
val .factorial = f(.n) fold(f .a x .b, series .n)

...and we can use an operator implied function...
val .factorial = f(.n) fold f{x}, series .n