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, and fold over explicit recursion when possible

Next Steps

Now that you understand functions, explore: