Functions
Functions are first-class citizens in Nostos. They are defined using a concise syntax and can be passed around like any other value.
Defining Functions
Simple functions are defined using the name(args) = body syntax.
add(a, b) = a + b
double(x) = x * 2
main() = {
result_add = add(5, 3) # result_add is 8
result_double = double(4) # result_double is 8
}
Lambda Expressions
Anonymous functions, or lambdas, are defined using the args => body syntax.
# Lambdas can be defined inside functions
main() = {
multiply = (a, b) => a * b
increment = x => x + 1
product = multiply(6, 7) # product is 42
next_val = increment(10) # next_val is 11
println(product)
}
# Or as regular functions at top-level
multiply(a, b) = a * b
increment(x) = x + 1
Higher-Order Functions
Higher-order functions (HOFs) are functions that can take other functions as arguments, or return functions as their results. This is a powerful feature for abstracting common patterns and writing more concise, reusable code.
Functions as Arguments
You can pass functions (including lambdas) as arguments to other functions.
# apply a function 'f' to a value 'x'
apply(f, x) = f(x)
# apply a function 'f' twice to a value 'x'
twice(f, x) = f(f(x))
main() = {
increment = x => x + 1
double = x => x * 2
# apply increment to 5
result1 = apply(increment, 5) # result1 is 6
# apply double twice to 3
result2 = twice(double, 3) # result2 is 12 (double(double(3)))
}
Functions as Return Values (Closures)
Functions can also return new functions, often "capturing" values from their environment. These are known as closures.
# Returns a function that adds 'n' to its input
make_adder(n) = x => x + n
main() = {
add5 = make_adder(5)
add10 = make_adder(10)
result1 = add5(2) # result1 is 7
result2 = add10(2) # result2 is 12
}
Common HOF Patterns (Map & Filter)
Higher-order functions are frequently used to implement powerful data transformation patterns like map and filter.
# The stdlib provides map and filter with list-first argument order
# This enables UFCS chaining: list.map(f).filter(p)
main() = {
numbers = [1, 2, 3, 4]
# Using stdlib map and filter (list first, function second)
squared_numbers = map(numbers, x => x * x)
# squared_numbers is [1, 4, 9, 16]
even_numbers = filter(numbers, x => x % 2 == 0)
# even_numbers is [2, 4]
# Or using method-style chaining (more readable)
result = numbers.map(x => x * x).filter(x => x > 5)
println(result) # [9, 16]
}
Method-Style Chaining
Nostos supports calling functions using dot notation, which enables fluent method-style chaining. When you write x.f(y), it's equivalent to f(x, y). This makes data transformation pipelines much more readable.
double(x) = x * 2
main() = {
# These are equivalent:
result1 = double(5)
result2 = 5.double()
# Chaining multiple operations
numbers = [1, 2, 3, 4, 5]
# Without chaining (nested calls, read inside-out)
result = sum(filter(map(numbers, x => x * 2), x => x > 2))
# With chaining (read left-to-right) - much cleaner!
result = numbers
.map(x => x * 2)
.filter(x => x > 2)
.sum()
# result is 24 (4 + 6 + 8 + 10)
println(result)
}
This works with any function - the value before the dot becomes the first argument:
# Define your own chainable functions
addOne(x) = x + 1
double(x) = x * 2
square(x) = x * x
main() = {
# Chain custom functions
result = 5.addOne().double().square() # ((5 + 1) * 2)^2 = 144
println(result)
# Works with list operations too
data = [1, 2, 3, 4, 5]
processed = data
.map(x => x * 10)
.filter(x => x > 20)
.take(2)
println(processed) # [30, 40]
println(calculate(-16)) # 4
}
Chaining Best Practices
- Use chaining for data transformation pipelines
- Break long chains across multiple lines for readability
- Prefer chaining when operations flow naturally left-to-right
- Use traditional function calls when order doesn't matter or for single operations
Named Parameters
Named parameters allow you to call functions with arguments in any order by specifying parameter names. This makes code more readable, especially for functions with many parameters.
# Define a function
greet(name, greeting) = greeting ++ ", " ++ name ++ "!"
# Call with named parameters (any order)
greet(name: "World", greeting: "Hello") # "Hello, World!"
greet(greeting: "Hi", name: "Alice") # "Hi, Alice!"
# Mix positional and named (positional must come first)
greet("Bob", greeting: "Hey") # "Hey, Bob!"
Named parameters work well with records:
type Point = { x: Int, y: Int, z: Int }
# Named args for record construction
p = Point(x: 10, y: 20, z: 30)
# Function with multiple point params
midpoint(p1, p2) = Point(
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
z: (p1.z + p2.z) / 2
)
# Call with named parameters for clarity
mid = midpoint(p1: origin, p2: target)
Default Parameter Values
Parameters can have default values, making them optional. Use the syntax param = defaultValue in the function definition.
# Parameters with default values
greet(name, greeting = "Hello", punctuation = "!") =
greeting ++ ", " ++ name ++ punctuation
# Call with all defaults
greet("World") # "Hello, World!"
# Override some defaults positionally
greet("World", "Hi") # "Hi, World!"
# Override all
greet("World", "Hey", "?") # "Hey, World?"
Default values can be any expression:
# Arithmetic expressions
addWithDefault(a, b = 10 * 2) = a + b
addWithDefault(5) # 25 (b defaults to 20)
# String defaults
wrap(text, prefix = "[", suffix = "]") = prefix ++ text ++ suffix
wrap("hello") # "[hello]"
# List defaults
appendItem(lst, item = 0) = lst ++ [item]
appendItem([1, 2, 3]) # [1, 2, 3, 0]
# Boolean defaults
check(value, invert = false) = if invert then !value else value
check(true) # true
check(true, true) # false
Combining Named and Default Parameters
Named parameters and defaults work together powerfully - you can skip optional parameters and specify only the ones you need.
greet(name, greeting = "Hello", punctuation = "!") =
greeting ++ ", " ++ name ++ punctuation
# Skip middle param using named arg
greet("World", punctuation: "?") # "Hello, World?"
# Use named arg to override specific default
greet("World", greeting: "Hey") # "Hey, World!"
# Named args in any order, skipping optionals
greet(punctuation: "!!!", name: "You", greeting: "Yo") # "Yo, You!!!"
# Only required arg, all defaults
greet(name: "Friend") # "Hello, Friend!"
Functions with all optional parameters:
makeRange(start = 0, stop = 10, step = 1) = (start, stop, step)
makeRange() # (0, 10, 1)
makeRange(5) # (5, 10, 1)
makeRange(stop: 20) # (0, 20, 1)
makeRange(step: 2, stop: 20) # (0, 20, 2)
makeRange(5, step: 3) # (5, 10, 3)
Best Practices
- Put required parameters before optional ones
- Use named parameters when calling functions with many arguments for clarity
- Use defaults for configuration options that have sensible common values
- Use named parameters to skip optional arguments you don't need