Functions
Functions are first-class values in Kit. They can be passed as arguments, returned from other functions, and stored in data structures. This guide covers everything you need to know about working with functions in Kit.
Function Definition
Functions are defined using the fn keyword followed by a parameter list and the
=> arrow syntax. The body of the function follows the arrow.
# Simple function with one parameter
square = fn(x) => x * x
# Function with multiple parameters
add = fn(a, b) => a + b
# Function with no parameters
get-random = fn() => 42
# Calling functions
println (square 5) # => 25
println (add 3 7) # => 10
println (get-random) # => 42
For multi-line functions, use indentation to define the function body:
factorial = fn(n) =>
if n <= 1 then
1
else
n * factorial (n - 1)
describe-number = fn(n) =>
is-even = n % 2 == 0
is-positive = n > 0
if is-even then
"${n} is even"
else
"${n} is odd"
Anonymous Functions and Lambdas
Functions don't need to be bound to a name. Anonymous functions (also called lambdas) are often used inline, especially when passing functions as arguments.
# Pass anonymous function to map
numbers = [1, 2, 3, 4, 5]
doubled = map (fn(x) => x * 2) numbers
# => [2, 4, 6, 8, 10]
# Filter with anonymous function
evens = filter (fn(x) => x % 2 == 0) numbers
# => [2, 4]
# Fold with anonymous function
sum = fold (fn(acc, x) => acc + x) 0 numbers
# => 15
# Immediately invoked function
result = (fn(x) => x * x) 7
# => 49
Closures
Functions in Kit are closures, which means they can capture and use variables from their surrounding scope. The captured variables remain accessible even after the outer function has returned.
# Simple closure capturing a variable
make-adder = fn(x) =>
fn(y) => x + y
add5 = make-adder 5
add10 = make-adder 10
println (add5 3) # => 8
println (add10 3) # => 13
# Counter using closure for state
make-counter = fn(start) =>
count = start
fn() =>
current = count
count = count + 1
current
counter = make-counter 0
println (counter) # => 0
println (counter) # => 1
println (counter) # => 2
# Closure capturing multiple values
make-multiplier-adder = fn(m, a) =>
fn(x) => (x * m) + a
double-plus-ten = make-multiplier-adder 2 10
println (double-plus-ten 5) # => 20
Currying and Partial Application
Currying is a technique where a multi-parameter function is transformed into a sequence of single-parameter functions. Kit functions support currying naturally.
# Traditional multi-parameter function
add = fn(a, b) => a + b
println (add 3 4) # => 7
# Curried version - returns a function
add-curried = fn(a) =>
fn(b) => a + b
add3 = add-curried 3
println (add3 4) # => 7
println (add3 10) # => 13
# Currying with three parameters
make-greeting = fn(greeting) =>
fn(name) =>
fn(punctuation) =>
"${greeting}, ${name}${punctuation}"
hello = make-greeting "Hello"
hello-alice = hello "Alice"
println (hello-alice "!") # => Hello, Alice!
println (hello-alice ".") # => Hello, Alice.
# Or call all at once
msg = make-greeting "Hi" "Bob" "?"
# => Hi, Bob?
# Practical example: configurable list processing
process-list = fn(transform) =>
fn(predicate) =>
fn(items) =>
items
|> filter predicate
|> map transform
process-evens = process-list (fn(x) => x * x) (fn(x) => x % 2 == 0)
result = process-evens [1, 2, 3, 4, 5]
# => [4, 16]
The Pipe Operator
Kit provides two pipe operators for different function calling conventions.
Data-First Pipe |>
The pipe operator |> takes the value on its left and passes it as the
first argument to the function on its right.
# x |> f a b => f x a b
# String operations
result = "hello world"
|> String.split " "
|> head
# => "hello"
# Map operations
config = {host: "localhost", port: 8080}
host = config |> Map.get "host"
# => Some "localhost"
# Chaining data-first operations
result = " hello world "
|> String.trim
|> String.split " "
|> head
# => "hello"
These functions expect the data/collection as their first argument:
| Module | Functions |
|---|---|
| String |
split, substring, replace, repeat,
count, pad-left, pad-right
|
| Map |
get, insert, delete, contains?,
has-key?
|
| Record | get, has-field? |
Data-Last Pipe |>>
The pipe operator |>> takes the value on its left and passes it as the
last argument to the function on its right.
# x |>> f a b => f a b x
# List transformations
result = [1, 2, 3, 4, 5]
|>> filter (fn(x) => x % 2 == 0) # Keep evens: [2, 4]
|>> map (fn(x) => x * x) # Square: [4, 16]
|>> fold (fn(a, x) => a + x) 0 # Sum: 20
# Complex data transformation
users = [
{name: "Alice", age: 25, active?: true},
{name: "Bob", age: 30, active?: false},
{name: "Carol", age: 28, active?: true}
]
active-names = users
|>> filter (fn(u) => u.active?)
|>> map (fn(u) => u.name)
# => ["Alice", "Carol"]
These functions expect the data/collection as their last argument:
| Module | Functions |
|---|---|
| List |
map, filter, fold, reduce,
each, contains?
|
| String | join |
| Seq |
map, filter, take, drop,
take-while, drop-while, flat-map, find,
any?, all?, nth, cons,
realize, partition, partition-by,
interpose
|
Field Accessor Shorthand
Kit provides a shorthand syntax for accessing fields in records. The syntax .field
is equivalent to fn(x) => x.field. This is particularly useful when working
with higher-order functions like map.
Basic Usage
# Create a field accessor
get-name = .name
# Use it like any function
person = {name: "Alice", age: 30}
println (get-name person) # => Alice
# Direct application (with space)
println (.name {name: "Bob"}) # => Bob
In Pipelines
Field accessors shine when used with map and other higher-order functions.
Note: Parentheses are required when passing a field accessor as an argument
due to parsing ambiguity with field access syntax.
# Extract names from a list of records
users = [{name: "Alice"}, {name: "Bob"}, {name: "Carol"}]
# With field accessor shorthand (parentheses required)
names = users |>> List.map (.name)
# => ["Alice", "Bob", "Carol"]
# Equivalent to the longer form:
names = users |>> List.map (fn(u) => u.name)
# Chain multiple field accesses
nested = [{inner: {value: 10}}, {inner: {value: 20}}]
values = nested
|>> List.map (.inner)
|>> List.map (.value)
# => [10, 20]
With Higher-Order Functions
# Use with custom HOFs
apply = fn(f, x) => f x
result = apply (.name) {name: "Test"}
# => "Test"
# Filter with field access and predicate
users = [{name: "Alice", active: true}, {name: "Bob", active: false}]
is-active? = fn(u) => u.active
active-names = users
|>> List.filter is-active?
|>> List.map (.name)
# => ["Alice"]
Higher-Order Functions
Higher-order functions are functions that take other functions as arguments or return functions as results. Kit's standard library provides several powerful higher-order functions for working with lists.
Map
Transform each element in a list:
numbers = [1, 2, 3, 4, 5]
# Square each number
squared = map (fn(x) => x * x) numbers
# => [1, 4, 9, 16, 25]
# Convert to strings
strings = map (fn(x) => "Number: ${x}") numbers
# => ["Number: 1", "Number: 2", ...]
# Extract field from records
people = [{name: "Alice"}, {name: "Bob"}]
names = map (fn(p) => p.name) people
# => ["Alice", "Bob"]
Filter
Keep only elements that satisfy a predicate:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Keep even numbers
evens = filter (fn(x) => x % 2 == 0) numbers
# => [2, 4, 6, 8, 10]
# Keep numbers greater than 5
large = filter (fn(x) => x > 5) numbers
# => [6, 7, 8, 9, 10]
# Filter records
users = [
{name: "Alice", age: 25},
{name: "Bob", age: 17},
{name: "Carol", age: 30}
]
adults = filter (fn(u) => u.age >= 18) users
# => [{name: "Alice", age: 25}, {name: "Carol", age: 30}]
Fold (Reduce)
Reduce a list to a single value by combining elements:
numbers = [1, 2, 3, 4, 5]
# Sum all numbers
sum = fold (fn(acc, x) => acc + x) 0 numbers
# => 15
# Product of all numbers
product = fold (fn(acc, x) => acc * x) 1 numbers
# => 120
# Find maximum
max-num = fold (fn(acc, x) => if x > acc then x else acc) 0 numbers
# => 5
# Build a string
words = ["Hello", "from", "Kit"]
sentence = fold (fn(acc, w) => "${acc} ${w}") "" words
# => " Hello from Kit"
# Count elements matching a condition
even-count = fold (fn(acc, x) => if x % 2 == 0 then acc + 1 else acc) 0 numbers
# => 2
Combining Higher-Order Functions
# Get the sum of squares of even numbers
result = [1, 2, 3, 4, 5, 6]
|> filter (fn(x) => x % 2 == 0)
|> map (fn(x) => x * x)
|> fold (fn(a, x) => a + x) 0
# => 56 (4 + 16 + 36)
# Custom higher-order function
map-with-index = fn(f, items) =>
helper = fn(lst, idx) =>
match lst
| [] -> []
| [x | rest] ->
result = f x idx
cons result (helper rest (idx + 1))
helper items 0
indexed = map-with-index (fn(x, i) => "${i}: ${x}") ["a", "b", "c"]
# => ["0: a", "1: b", "2: c"]
Recursion
Recursion is when a function calls itself. It's a fundamental technique in functional programming, often used instead of loops. Kit supports both simple recursion and tail-recursive functions.
Basic Recursion
# Classic factorial
factorial = fn(n) =>
if n <= 1 then
1
else
n * factorial (n - 1)
println (factorial 5) # => 120
# Fibonacci sequence
fib = fn(n) =>
if n <= 1 then
n
else
fib (n - 1) + fib (n - 2)
println (fib 7) # => 13
# Sum of list (recursive)
sum = fn(lst) =>
match lst
| [] -> 0
| [x | rest] -> x + sum rest
println (sum [1, 2, 3, 4]) # => 10
Tail Recursion
Tail-recursive functions make the recursive call as their final action, with no further computation after the call returns. This allows the compiler to optimize the recursion into a loop.
# Tail-recursive factorial with accumulator
factorial-tail = fn(n) =>
helper = fn(n, acc) =>
if n <= 1 then
acc
else
helper (n - 1) (acc * n)
helper n 1
println (factorial-tail 5) # => 120
# Tail-recursive sum
sum-tail = fn(lst) =>
helper = fn(items, acc) =>
match items
| [] -> acc
| [x | rest] -> helper rest (acc + x)
helper lst 0
println (sum-tail [1, 2, 3, 4]) # => 10
# Tail-recursive reverse
reverse-tail = fn(lst) =>
helper = fn(items, acc) =>
match items
| [] -> acc
| [x | rest] -> helper rest (cons x acc)
helper lst []
println (reverse-tail [1, 2, 3]) # => [3, 2, 1]
Mutual Recursion
Two or more functions that call each other:
# Check if a number is even or odd using mutual recursion
is-even? = fn(n) =>
if n == 0 then
true
else
is-odd? (n - 1)
is-odd? = fn(n) =>
if n == 0 then
false
else
is-even? (n - 1)
println (is-even? 4) # => true
println (is-odd? 5) # => true
Function Composition
Combining functions to create new functions:
# Compose two functions
compose = fn(f, g) =>
fn(x) => f (g x)
add1 = fn(x) => x + 1
times2 = fn(x) => x * 2
add1-then-times2 = compose times2 add1
println (add1-then-times2 5) # => 12 ((5 + 1) * 2)
# Compose multiple functions
pipeline = fn(x) =>
x
|> add1
|> times2
|> add1
println (pipeline 5) # => 13 (((5 + 1) * 2) + 1)
Best Practices
- Use descriptive names - Function names should clearly describe what they do
- Keep functions small - Each function should do one thing well
- Prefer pure functions - Functions that don't have side effects and always return the same output for the same input
- Use the pipe operator - Makes data transformations more readable
- Prefer tail recursion - For better performance with recursive functions
- Leverage closures - Capture state or configuration in a clean way
-
Use higher-order functions - Prefer
map,filter, andfoldover explicit recursion when possible
Next Steps
Now that you understand functions, explore:
- List Functions - All available list operations
- Language Tour - Other Kit language features
- Standard Library - Built-in functions and modules