Functions
Defining a Function
Use def to define a named function. The last expression is implicitly returned — return is optional at the end.
def add(a, b)
a + b
end
puts add(3, 4) # 7
return is still useful for early exits:
def divide(a, b)
return nil if b == 0
a / b
end
puts divide(10, 2) # 5.0
puts divide(10, 0) # nil
Default Parameters
Parameters can have default values. Required parameters must come before optional ones.
def greet(name, msg = "Hello", punct = "!")
puts "#{msg}, #{name}#{punct}"
end
greet("Alice") # Hello, Alice!
greet("Bob", "Hi") # Hi, Bob!
greet("Carol", "Hey", ".") # Hey, Carol.
Named Arguments
Some built-in functions accept named arguments with key: value syntax:
puts paste("2025", "03", "14", sep: "-") # 2025-03-14
puts sprintf("%-10s %d", "Frankie", 1)
Multiple Return Values
Return a vector and destructure it at the call site:
def min_max(v)
[min(v), max(v)]
end
lo, hi = min_max([3, 1, 4, 1, 5, 9])
puts "#{lo}..#{hi}" # 1..9
def stats(v)
[mean(v), median(v), stdev(v)]
end
avg, med, sd = stats([10, 20, 30, 40, 50])
puts avg # 30.0
puts med # 30
Recursion
Functions can call themselves. Frankie supports full recursion.
def factorial(n)
return 1 if n <= 1
n * factorial(n - 1)
end
puts factorial(10) # 3628800
def fib(n)
return n if n <= 1
fib(n - 1) + fib(n - 2)
end
puts fib(10) # 55
Lambdas — Functions as Values
Use -> to create an anonymous function that can be stored, passed, and called later.
double = ->(x) { x * 2 }
puts double.call(5) # 10
add = ->(a, b) { a + b }
puts add.call(3, 4) # 7
Multi-statement bodies use do...end:
clamp = ->(n, lo, hi) do
return lo if n < lo
return hi if n > hi
n
end
puts clamp.call(150, 0, 100) # 100
puts clamp.call(-5, 0, 100) # 0
puts clamp.call(42, 0, 100) # 42
Lambdas accept default parameters:
greet = ->(name, prefix = "Hello") { "#{prefix}, #{name}!" }
puts greet.call("Alice") # Hello, Alice!
puts greet.call("Bob", "Hi") # Hi, Bob!
Lambdas as Arguments
Pass lambdas to functions for higher-order patterns:
def apply(fn, val)
fn.call(val)
end
square = ->(x) { x * x }
puts apply(square, 6) # 36
# Store in a vector and call each
transforms = [
->(x) { x + 1 },
->(x) { x * 2 },
->(x) { x ** 2 }
]
transforms.each do |fn|
puts fn.call(5)
end
# 6
# 10
# 25
Blocks
Blocks are anonymous chunks of code passed directly to a method call with do...end or { }. They are not stored — they run once as part of the method.
[1, 2, 3].each do |x|
puts x * 2
end
result = [1, 2, 3].map do |x|
x * x
end
puts result # [1, 4, 9]
Single-line blocks can use braces:
[1, 2, 3].each do |x| puts x end
Blocks are the foundation of Frankie's iterator system — .map, .select, .reduce, .each_with_object, and all other iterators take blocks.
The Pipe Operator
Chain function calls without nesting:
data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
data |> sum |> puts # 39
data |> mean |> puts # 3.9
def double(x)
x * 2
end
10 |> double |> puts # 20
Scope
Variables defined outside a function are readable inside it. Variables assigned inside a function are local to it — they do not affect the outer scope.
x = 10
def read_outer
puts x # 10 — outer x is readable
end
read_outer
def set_local
x = 99 # local to set_local
puts x # 99
end
set_local
puts x # 10 — outer x unchanged
Multi-file Programs
Split code across files with require. Each file is loaded at most once.
# lib/math_utils.fk
def circle_area(r)
PI * r * r
end
# main.fk
require "lib/math_utils"
puts circle_area(5) # 78.539...
require resolves paths relative to the current working directory. The .fk extension is optional.