Functions and Closures in GoloScript
A comprehensive guide to functions, lambdas, and closures in GoloScript, with practical examples and use cases.
Introduction
Functions and closures are fundamental building blocks in GoloScript. Functions allow you to encapsulate reusable code, while closures enable powerful functional programming patterns by combining functions with variable capture.
Key Concepts
- Functions: Named blocks of code that can accept parameters and return values
- Lambdas: Anonymous functions defined inline (also called closures)
- Closures: Functions that capture variables from their enclosing scope
- Higher-order functions: Functions that work with other functions (as parameters or return values)
Function Syntax
Basic Function with Parameters
function add = |a, b| {
return a + b
}
add(5, 3) # Returns 8
Function Without Parameters
# Syntax without pipes
function getGreeting = {
return "Hello from GoloScript!"
}
getGreeting() # Returns "Hello from GoloScript!"
Implicit Return
The last expression in a function is automatically returned:
function square = |n| {
n * n # Automatically returned
}
function isEven = |n| {
n % 2 == 0
}
square(7) # Returns 49
isEven(10) # Returns true
Multiple Return Values
Return arrays (tuples) to return multiple values:
function divMod = |a, b| {
let quotient = a / b
let remainder = a % b
return [quotient, remainder]
}
let result = divMod(17, 5)
println(result[0]) # 3 (quotient)
println(result[1]) # 2 (remainder)
Nested Functions
Define local functions within other functions:
function calculate = |a, b| {
function helper = |x| {
return x * 2
}
return helper(a) + helper(b)
}
calculate(3, 5) # Returns 16 (3*2 + 5*2)
Variadic Functions (Varargs)
GoloScript supports variable arguments with the args... syntax (similar to JavaScript’s rest parameters).
Varargs Only
# Function accepts variable number of arguments
function test = |args...| {
println("Number of arguments:", len(args))
return args
}
println(test()) # [] (empty array)
println(test(1, 2, 3)) # [1, 2, 3]
Mixed Parameters with Varargs
You can combine regular parameters with varargs. Important: only the last parameter can be varargs.
function test = |a, b...| {
println("a:", a) # First argument
println("b:", b) # Rest of arguments (array)
println("type of b:", type(b))
println("length of b:", len(b))
return b
}
let result = test(1, 2, 3, 4)
println("result:", result) # [2, 3, 4]
println("result[0]:", result[0]) # 2
println("result[1]:", result[1]) # 3
Practical Example: sum Function
function sum = |numbers...| {
var total = 0
foreach n in numbers {
total = total + n
}
return total
}
println(sum(1, 2, 3, 4, 5)) # 15
println(sum(10, 20, 30)) # 60
println(sum()) # 0
Alternative: Passing Arrays Explicitly
If you prefer, you can always pass an array explicitly:
function sumAll = |numbers| {
var total = 0
foreach n in numbers {
total = total + n
}
return total
}
sumAll(array(1, 2, 3, 4, 5)) # Returns 15
Recursive Functions
Functions can call themselves:
function factorial = |n| {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
function fibonacci = |n| {
if n <= 1 {
return n
}
return fibonacci(n - 1) + fibonacci(n - 2)
}
factorial(5) # Returns 120
fibonacci(7) # Returns 13
Pure vs Impure Functions
Pure functions always return the same output for the same input with no side effects:
function purePlus = |a, b| {
return a + b
}
purePlus(2, 3) # Always returns 5
purePlus(2, 3) # Still returns 5
Impure functions modify external state or have side effects:
var state = 0
function impurePlus = |a, b| {
state = state + 1
return a + b + state
}
impurePlus(2, 3) # Returns 6 (state is now 1)
impurePlus(2, 3) # Returns 7 (state is now 2)
Closure Syntax
Closures are anonymous functions (lambdas) that can be assigned to variables. GoloScript supports multiple syntactic variations.
Simple Lambda (Single Parameter, Compact)
let double = |x| -> x * 2
let triple = |x| -> x * 3
let add = |a, b| -> a + b
double(5) # Returns 10
triple(5) # Returns 15
add(3, 7) # Returns 10
Lambda with Block Body
# Single parameter with block
let double1 = |x| {
return x * 2
}
# Multiple parameters with block
let add1 = |a, b| {
return a + b
}
double1(5) # Returns 10
add1(3, 7) # Returns 10
Closure Without Parameters - Arrow Syntax
let getMessage = -> "Hello from closure!"
let getNumber = -> 42
let getPi = -> 3.14159
getMessage() # Returns "Hello from closure!"
getNumber() # Returns 42
Closure Without Parameters - Block Syntax
let c2 = {
"Block syntax"
}
c2() # Returns "Block syntax"
Closure with Multiple Statements
let complex = |x| -> {
let doubled = x * 2
let squared = doubled * doubled
return squared
}
complex(3) # Returns 36 (3*2 = 6, 6*6 = 36)
Array of Closures
let funcs = [
-> 10,
-> 20,
-> 30
]
funcs[0]() # Returns 10
funcs[1]() # Returns 20
funcs[2]() # Returns 30
Conditional Closures
let isWeekend = -> false
let message = if isWeekend() {
-> "Rest!"
} else {
-> "Work!"
}
message() # Returns "Work!"
Variable Capture
One of the most powerful features of closures is their ability to “capture” variables from their surrounding scope.
Basic Variable Capture
function makeMultiplier = |factor| {
return |x| -> x * factor
}
let times2 = makeMultiplier(2)
let times5 = makeMultiplier(5)
let times10 = makeMultiplier(10)
times2(7) # Returns 14
times5(7) # Returns 35
times10(7) # Returns 70
Each closure captures its own copy of the factor parameter.
Private State with Closures
Closures can maintain private state:
function makeCounter = {
var count = 0
return {
count = count + 1
return count
}
}
let counter1 = makeCounter()
let counter2 = makeCounter()
counter1() # Returns 1
counter1() # Returns 2
counter1() # Returns 3
counter2() # Returns 1 (separate counter)
counter2() # Returns 2
Each counter has its own independent count variable.
Multiple Functions Sharing State
A factory can return multiple closures that share the same private state:
function makeBank = |initialBalance| {
var balance = initialBalance
let deposit = |amount| {
balance = balance + amount
return balance
}
let withdraw = |amount| {
if amount > balance {
println("Insufficient funds!")
return balance
}
balance = balance - amount
return balance
}
let getBalance = {
return balance
}
return [deposit, withdraw, getBalance]
}
let account = makeBank(100)
let depositFn = account[0]
let withdrawFn = account[1]
let getBalanceFn = account[2]
getBalanceFn() # 100
depositFn(50) # 150
withdrawFn(30) # 120
getBalanceFn() # 120
All three functions share and can modify the same balance variable.
Nested Closures
Closures can capture from multiple levels:
function makeAdder = |a| {
return |b| {
return |c| {
return a + b + c
}
}
}
let add5 = makeAdder(5)
let add5and10 = add5(10)
let result = add5and10(3)
println(result) # 18 (5 + 10 + 3)
Array of Closures with Captured Variables
let operations = array(
|x| -> x + 1,
|x| -> x * 2,
|x| -> x - 5,
|x| -> x / 2
)
let value = 10
operations: get(0)(value) # 11
operations: get(1)(value) # 20
operations: get(2)(value) # 5
operations: get(3)(value) # 5
Lazy Evaluation
Closures are not evaluated until called, enabling lazy evaluation:
let expensiveCalc = -> {
println("Computing expensive result...")
return 42 * 42
}
println("Closure created but not called yet")
let result = expensiveCalc() # Now it computes
println(result) # 1764
Higher-Order Functions
Higher-order functions take functions as parameters or return functions.
Functions Taking Functions
function apply = |f, x| {
return f(x)
}
let double = |x| -> x * 2
let square = |x| -> x * x
apply(double, 5) # Returns 10
apply(square, 5) # Returns 25
Functions Returning Functions
function makeAdder = |n| {
return |x| -> x + n
}
let add5 = makeAdder(5)
let add10 = makeAdder(10)
add5(3) # Returns 8
add10(3) # Returns 13
Function Composition
Combine functions by applying one to the result of another:
function compose = |f, g| {
return |x| -> f(g(x))
}
let addOne = |x| -> x + 1
let multiplyByTwo = |x| -> x * 2
let addThenMultiply = compose(multiplyByTwo, addOne)
let multiplyThenAdd = compose(addOne, multiplyByTwo)
addThenMultiply(5) # (5 + 1) * 2 = 12
multiplyThenAdd(5) # (5 * 2) + 1 = 11
Applying a Function Multiple Times
function applyTwice = |f, x| {
return f(f(x))
}
function applyNTimes = |f, n, x| {
var result = x
var i = 0
while i < n {
result = f(result)
i = i + 1
}
return result
}
let increment = |x| -> x + 1
applyTwice(increment, 10) # Returns 12
applyNTimes(increment, 5, 10) # Returns 15
Map - Transform Array Elements
let numbers = array(1, 2, 3, 4, 5)
let doubled = numbers: map(|x| -> x * 2)
let squared = numbers: map(|x| -> x * x)
println(numbers) # [1, 2, 3, 4, 5]
println(doubled) # [2, 4, 6, 8, 10]
println(squared) # [1, 4, 9, 16, 25]
Filter - Select Array Elements
let isEven = |x| -> x % 2 == 0
let isPositive = |x| -> x > 0
let values = array(-2, -1, 0, 1, 2, 3, 4, 5)
let evens = values: filter(isEven)
let positives = values: filter(isPositive)
println(values) # [-2, -1, 0, 1, 2, 3, 4, 5]
println(evens) # [-2, 0, 2, 4]
println(positives) # [1, 2, 3, 4, 5]
Reduce - Fold to a Single Value
let nums = array(1, 2, 3, 4, 5)
let sum = nums: reduce(0, |acc, x| -> acc + x)
let product = nums: reduce(1, |acc, x| -> acc * x)
println(sum) # 15
println(product) # 120
Currying and Partial Application
function curry = |f| {
return |a| -> |b| -> f(a, b)
}
let addNormal = |a, b| -> a + b
let addCurried = curry(addNormal)
let add3 = addCurried(3)
let add7 = addCurried(7)
add3(5) # Returns 8
add7(5) # Returns 12
Practical Use Cases
1. Factory Functions
Create objects or configurations with closures:
function createPerson = |name, age| {
let getName = -> name
let getAge = -> age
let greet = -> "Hello, I'm " + name + " and I'm " + str(age)
return [getName, getAge, greet]
}
let alice = createPerson("Alice", 30)
let bob = createPerson("Bob", 25)
println(alice[2]()) # "Hello, I'm Alice and I'm 30"
println(bob[2]()) # "Hello, I'm Bob and I'm 25"
2. Configuration Builders
Build and configure objects step-by-step:
function createConfig = {
var host = "localhost"
var port = 8080
var debug = false
let setHost = |h| { host = h }
let setPort = |p| { port = p }
let setDebug = |d| { debug = d }
let build = -> "Config: " + host + ":" + str(port) + " (debug=" + str(debug) + ")"
return [setHost, setPort, setDebug, build]
}
let config = createConfig()
config[0]("api.example.com")
config[1](443)
config[2](true)
println(config[3]()) # "Config: api.example.com:443 (debug=true)"
3. Event Handlers
Simulate event handling with closures:
function createButton = |label| {
var clickCount = 0
let onClick = |handler| {
clickCount = clickCount + 1
handler(label, clickCount)
}
return onClick
}
let button = createButton("Submit")
let handleClick = |label, count| {
println("Button '" + label + "' clicked " + str(count) + " times")
}
button(handleClick) # "Button 'Submit' clicked 1 times"
button(handleClick) # "Button 'Submit' clicked 2 times"
button(handleClick) # "Button 'Submit' clicked 3 times"
4. Memoization (Caching)
Cache function results to avoid recomputation:
function memoize = |fn| {
var cache = map[]
return |arg| {
let cached = cache: get(str(arg))
if not isNull(cached) {
println("(from cache)")
return cached
}
println("(computing...)")
let result = fn(arg)
cache = map[cache, [str(arg), result]]
return result
}
}
let expensiveFn = |n| -> n * n
let memoized = memoize(expensiveFn)
memoized(5) # "(computing...)" Returns 25
memoized(5) # "(from cache)" Returns 25
memoized(7) # "(computing...)" Returns 49
5. Pipeline of Transformations
Chain multiple transformations:
function pipeline = |value, funcs| {
var result = value
foreach fn in funcs {
result = fn(result)
}
return result
}
let transforms = array(
|x| -> x + 1,
|x| -> x * 2,
|x| -> x - 3
)
let result = pipeline(10, transforms)
# 10 -> (+1) -> (*2) -> (-3)
# 10 + 1 = 11
# 11 * 2 = 22
# 22 - 3 = 19
println(result) # 19
6. Partial Application
Bind some arguments to create specialized versions:
function partial = |fn, arg1| {
return |arg2| -> fn(arg1, arg2)
}
let multiply = |a, b| -> a * b
let double = partial(multiply, 2)
let triple = partial(multiply, 3)
double(5) # Returns 10
triple(5) # Returns 15
7. Lazy Sequences
Generate values on-demand:
function range = |start, end| {
var current = start
return {
if current <= end {
let value = current
current = current + 1
return value
}
return null
}
}
let numbers = range(1, 5)
println(numbers()) # 1
println(numbers()) # 2
println(numbers()) # 3
println(numbers()) # 4
println(numbers()) # 5
println(numbers()) # null (exhausted)
Syntax Comparison
Closure Syntax Overview
| Use Case | Syntax | Example | Notes |
|---|---|---|---|
| No params, compact | -> expression |
-> 42 |
Most concise |
| No params, block | { ... } |
{ "text" } |
Alternative syntax |
| One param, compact | |x| -> expr |
|x| -> x * 2 |
Clean and readable |
| One param, block | |x| { ... } |
|x| { return x * 2 } |
More explicit |
| Multiple params, compact | |a,b| -> expr |
|a,b| -> a + b |
Preferred for simple cases |
| Multiple params, block | |a,b| { ... } |
|a,b| { return a + b } |
When multiple statements needed |
| Complex body | |x| -> { ... } |
|x| -> { let y = x * 2; return y } |
Multi-statement with arrow |
Function vs Closure
| Feature | Function | Closure |
|---|---|---|
| Declaration | function name = ... |
let name = ... |
| Parameters | Required pipes: |a,b| |
Same syntax |
| Anonymous | No (must be named) | Yes (typically) |
| Variable capture | Limited | Full support |
| Use in collections | Can reference by name | Can store directly |
| Return syntax | Explicit or implicit | Implicit in arrow form |
Quick Reference
Function Declaration Patterns
# Function with parameters
function add = |a, b| -> a + b
# Function without parameters (returns immediately)
function greet = -> "Hello"
# Function with block
function calculate = |x| {
let y = x * 2
y + 5
}
# Nested function
function outer = {
function inner = |x| -> x * 2
return inner(5)
}
Closure Patterns
# Simple lambda
let double = |x| -> x * 2
# Closure with variable capture
let makeMultiplier = |factor| -> |x| -> x * factor
let times3 = makeMultiplier(3)
# Closure with state
let makeCounter = -> {
var count = 0
return { count = count + 1; return count }
}
# Closure taking a function
let apply = |f, x| -> f(x)
# Closure returning a function
let compose = |f, g| -> |x| -> f(g(x))
Common Higher-Order Function Patterns
# Map
let doubled = numbers: map(|x| -> x * 2)
# Filter
let evens = nums: filter(|x| -> x % 2 == 0)
# Reduce
let sum = nums: reduce(|acc, x| -> acc + x, 0)
# Compose
let composed = compose(f, g)
# Partial application
let double = partial(multiply, 2)
# Currying
let curriedAdd = curry(|a, b| -> a + b)
Key Takeaways
-
Functions are first-class values in GoloScript that can be passed around and used just like any other value
-
Closures capture variables from their enclosing scope, enabling powerful abstraction patterns
-
Variable capture creates private state and factory patterns without needing classes
-
Higher-order functions (map, filter, reduce, compose) enable functional programming styles
-
Multiple syntaxes offer flexibility:
- Arrow syntax (
->) for concise lambdas - Block syntax (
{ }) for multi-statement bodies - Compact form for simple expressions
- Arrow syntax (
-
Lazy evaluation is natural with closures - code inside isn’t executed until the closure is called
-
Composition and partial application allow building complex functions from simpler ones