What is Functional Programming in V?
Functional programming (FP) is a paradigm where computation is treated as the evaluation of mathematical functions, avoiding mutable state and side effects wherever possible. V, as a modern systems language, embraces many functional programming concepts natively — even though it is not a purely functional language like Haskell. V gives you the tools to write declarative, predictable, and testable code by leaning on immutability, higher-order functions, closures, and function composition.
In V, functional programming is not an all-or-nothing proposition. You can adopt FP style gradually, mixing it with imperative code when performance or pragmatism calls for it. The key insight is that V's default immutability and its elegant support for first-class functions make functional patterns feel natural rather than bolted on.
Core Functional Concepts in V
- Immutability by default — variables are immutable unless explicitly marked with
mut - First-class functions — functions can be assigned to variables, passed as arguments, and returned from other functions
- Higher-order functions — functions that take or return other functions
- Closures — functions that capture variables from their enclosing scope
- Pure functions — functions with no side effects, always returning the same output for the same input
- Expression-oriented syntax — many constructs return values, enabling concise function bodies
Why Functional Programming Matters in V
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Embracing functional programming in V brings several concrete benefits:
- Predictability — pure functions are deterministic. Given the same inputs, they always produce the same output. This eliminates entire categories of bugs related to hidden state.
- Testability — pure functions require no complex mocking or state setup. You test inputs against expected outputs, making unit tests trivial to write and maintain.
- Concurrency safety — immutable data is inherently thread-safe. When data cannot change, you never need locks to protect it. This aligns perfectly with V's emphasis on easy concurrency.
- Code clarity — functional pipelines using
map,filter, andreduceexpress intent directly. A reader can understand the data transformation without mentally stepping through loop iterations. - Reusability — small, focused functions compose together in countless ways, reducing duplication across your codebase.
In V specifically, FP patterns help you leverage the compiler's static guarantees. The type system catches mismatches in function pipelines at compile time, so you get the safety of functional design with the performance of compiled native code.
How to Use Functional Programming in V
Immutability and mut
By default, every variable in V is immutable. You must opt into mutability explicitly. This design choice naturally pushes your code toward functional style.
// Immutable by default — the functional way
name := "Vlang"
// name = "V2" // compile error: cannot reassign immutable variable
// Explicit mutability when you genuinely need it
mut counter := 0
counter = counter + 1
println(counter) // 1
// Immutable struct fields are the norm
struct User {
id int
name string
}
u := User{id: 1, name: "Alice"}
// u.name = "Bob" // compile error: field is immutable
When you design functions, prefer returning new values rather than mutating arguments. This keeps callers predictable and makes your functions pure.
// Bad: mutates the argument (impure, harder to reason about)
fn add_title(mut user User) {
user.name = "Dr. " + user.name
}
// Good: returns a new value (pure, predictable)
fn with_title(user User) User {
return User{
id: user.id,
name: "Dr. " + user.name,
}
}
Higher-Order Functions
Higher-order functions are functions that operate on other functions. In V, functions are first-class values that can be passed around freely. The function type syntax uses fn followed by parameter types and the return type.
// A function type: takes int, returns int
type IntTransform = fn(int) int
// A higher-order function that applies a transformation twice
fn apply_twice(f IntTransform, x int) int {
return f(f(x))
}
fn double(n int) int {
return n * 2
}
fn increment(n int) int {
return n + 1
}
fn main() {
result1 := apply_twice(double, 3) // double(double(3)) = 12
result2 := apply_twice(increment, 5) // increment(increment(5)) = 7
println("double twice: ${result1}")
println("increment twice: ${result2}")
}
You can also assign anonymous functions (lambdas) to variables inline:
square := fn(x int) int {
return x * x
}
// Using the function type alias
transform := IntTransform(square)
println(apply_twice(transform, 4)) // square(square(4)) = 256
Closures
A closure is a function that captures variables from its surrounding scope. In V, closures are created automatically when you define a function that references outer variables.
// make_adder returns a closure that captures 'amount'
fn make_adder(amount int) fn(int) int {
return fn(x int) int {
return x + amount // 'amount' is captured from the outer scope
}
}
fn main() {
add_five := make_adder(5)
add_ten := make_adder(10)
println(add_five(20)) // 25
println(add_ten(20)) // 30
// Each closure holds its own captured value
println(add_five(100)) // 105
println(add_ten(100)) // 110
}
Closures are particularly useful for creating configurable behavior without resorting to objects or global state:
// Create a threshold checker as a closure
fn threshold_checker(threshold int) fn(int) bool {
return fn(value int) bool {
return value > threshold
}
}
fn main() {
is_positive := threshold_checker(0)
is_hot := threshold_checker(30)
temperatures := [22, 31, 18, 35, 27]
println(temperatures.filter(is_positive)) // all values (all > 0)
println(temperatures.filter(is_hot)) // [31, 35]
}
Pure Functions
A pure function has no side effects and always returns the same result for the same input. It doesn't modify global state, perform I/O, or mutate its arguments. Pure functions are the backbone of functional programming.
// Pure function: same input → same output, no side effects
fn factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
// Pure function operating on a slice without mutating the original
fn with_each_doubled(numbers []int) []int {
mut result := []int{len: numbers.len, cap: numbers.len}
for i, val in numbers {
result[i] = val * 2
}
return result
}
// Impure function: mutates the original slice (avoid when possible)
fn double_in_place(mut numbers []int) {
for i, mut val in numbers {
numbers[i] = val * 2
}
}
When you must perform I/O or modify state, isolate that impurity at the boundaries of your program. Keep the core logic pure:
// Core business logic is pure
fn calculate_discount(price f64, loyalty_years int) f64 {
base_discount := if loyalty_years > 5 { 0.15 } else { 0.05 }
return price * (1.0 - base_discount)
}
// Impure I/O happens only at the boundary
fn main() {
// Impure: reading input
price_input := 99.99
years_input := 7
// Pure computation
final_price := calculate_discount(price_input, years_input)
// Impure: writing output
println("Final price: ${final_price}")
}
Map, Filter, Reduce Patterns
V's built-in array methods provide a rich set of functional transformations. These methods take closures and return new arrays (or single values), leaving the original unchanged.
fn main() {
numbers := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// map: transform each element, returns new array
doubled := numbers.map(fn(n int) int {
return n * 2
})
println(doubled) // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// filter: keep elements where closure returns true
evens := numbers.filter(fn(n int) bool {
return n % 2 == 0
})
println(evens) // [2, 4, 6, 8, 10]
// Chaining map and filter together
result := numbers
.filter(fn(n int) bool { return n % 2 != 0 })
.map(fn(n int) int { return n * n })
println(result) // [1, 9, 25, 49, 81]
// any: returns true if any element satisfies the condition
has_large := numbers.any(fn(n int) bool { return n > 7 })
println(has_large) // true
// all: returns true if all elements satisfy the condition
all_positive := numbers.all(fn(n int) bool { return n > 0 })
println(all_positive) // true
}
For reduce (fold) operations, V provides a reduce method that accumulates a value across the array:
fn main() {
numbers := [1, 2, 3, 4, 5]
// reduce: accumulate a value (sum all numbers)
total := numbers.reduce(fn(acc int, n int) int {
return acc + n
}, 0)
println(total) // 15
// reduce for finding the maximum
max_val := numbers.reduce(fn(acc int, n int) int {
return if n > acc { n } else { acc }
}, numbers[0])
println(max_val) // 5
// More complex reduction: build a map of frequencies
words := ["apple", "banana", "apple", "cherry", "banana", "apple"]
freq := words.reduce(fn(acc map[string]int, w string) map[string]int {
mut m := acc.clone()
m[w] = m[w] + 1
return m
}, map[string]int{})
println(freq) // {'apple': 3, 'banana': 2, 'cherry': 1}
}
Function Composition
Function composition means combining simple functions to build more complex ones. In V, you can manually compose functions by wrapping calls or by writing a dedicated compose utility.
// Simple functions we want to compose
fn add_one(n int) int { return n + 1 }
fn square(n int) int { return n * n }
fn to_string(n int) string { return "Value: ${n}" }
// Manual composition by nesting calls
fn main() {
n := 5
result := to_string(square(add_one(n)))
println(result) // "Value: 36"
}
// A generic compose function for two-argument pipelines
fn compose_int(a fn(int) int, b fn(int) int) fn(int) int {
return fn(x int) int {
return b(a(x))
}
}
fn main() {
add_then_square := compose_int(add_one, square)
println(add_then_square(5)) // 36
// Build a pipeline of transformations
numbers := [1, 2, 3, 4, 5]
transform := compose_int(
fn(n int) int { return n * 3 },
fn(n int) int { return n + 10 },
)
result := numbers.map(transform)
println(result) // [13, 16, 19, 22, 25]
}
For more flexibility, you can build a pipeline function that composes an arbitrary number of functions in sequence:
// Pipeline applies a series of transformations in order
fn pipeline(input int, steps []fn(int) int) int {
mut current := input
for step in steps {
current = step(current)
}
return current
}
fn main() {
steps := [
fn(n int) int { return n + 10 },
fn(n int) int { return n * 2 },
fn(n int) int { return n - 3 },
]
result := pipeline(5, steps)
// ((5 + 10) * 2) - 3 = 27
println(result) // 27
}
Recursion
Recursion replaces loops in pure functional style. V supports recursive functions naturally. For deep recursion, prefer tail-recursive patterns when possible, though V does not automatically optimize tail calls.
// Classic recursive factorial
fn factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
// Tail-recursive factorial (manually structured)
fn tail_factorial(n int, accumulator int) int {
if n <= 1 {
return accumulator
}
return tail_factorial(n - 1, accumulator * n)
}
fn factorial_tr(n int) int {
return tail_factorial(n, 1)
}
fn main() {
println(factorial(5)) // 120
println(factorial_tr(5)) // 120
}
Recursion works beautifully with immutable data structures like linked lists or trees:
// Recursive sum of array elements (functional style)
fn sum_recursive(numbers []int, index int) int {
if index >= numbers.len {
return 0
}
return numbers[index] + sum_recursive(numbers, index + 1)
}
fn sum(numbers []int) int {
return sum_recursive(numbers, 0)
}
// Recursive tree traversal
struct Node {
value int
children []Node
}
fn sum_tree(node Node) int {
mut total := node.value
for child in node.children {
total += sum_tree(child)
}
return total
}
Best Practices for Functional Programming in V
- Default to immutable — avoid
mutunless you have a compelling reason. If you find yourself reaching formutout of habit, pause and consider whether a functional approach would be cleaner. - Keep functions small and focused — a function should do one thing well. If your function spans more than 10–15 lines, it probably handles multiple responsibilities. Break it into smaller, composable pieces.
- Separate pure and impure code — push I/O, database access, and mutable state to the outer edges of your program. Keep the core logic pure so it remains easy to test and reason about.
- Use array methods over manual loops — prefer
.map(),.filter(),.reduce(),.any(), and.all()instead of writing imperativeforloops. The intent is clearer, and the risk of off-by-one errors disappears. - Name closures meaningfully — when passing anonymous functions, give them descriptive variable names or keep them short enough to understand at a glance. A well-named closure like
is_validorto_uppercasecommunicates intent instantly. - Avoid side effects inside callbacks — closures passed to
maporfiltershould be pure. Don't modify external variables or perform I/O inside them — it makes behavior unpredictable and defeats the purpose of functional pipelines. - Leverage the type system — define explicit function types with
type FnTransform = fn(int) intwhen a function signature appears repeatedly. This improves readability and catches type mismatches at compile time. - Compose, don't nest — when you have multiple transformations, chain them with
.map().filter()rather than deeply nesting function calls. If you need reusable pipelines, build a compose utility or a pipeline function. - Test pure functions exhaustively — because pure functions have no hidden dependencies, you can test them with simple table-driven tests covering edge cases without any mocking framework.
- Know when to break the rules — functional programming is a tool, not a dogma. In performance-critical sections, a well-placed mutable loop may be the right choice. V lets you seamlessly transition between FP and imperative styles as needed.
Putting It All Together: A Complete Example
Here is a realistic example that combines all the patterns discussed above — immutability, pure functions, closures, map/filter/reduce, and composition — to process a list of transactions:
struct Transaction {
id int
amount f64
category string
approved bool
}
// Pure function: categorize a single transaction
fn is_large(amount f64) bool {
return amount > 100.0
}
// Pure function: calculate a processing fee
fn processing_fee(amount f64) f64 {
return amount * 0.02
}
// Pure function: format transaction summary
fn format_summary(id int, final_amount f64) string {
return "TX-${id}: ${final_amount:.2f}"
}
fn main() {
transactions := [
Transaction{id: 1, amount: 50.0, category: "food", approved: true},
Transaction{id: 2, amount: 250.0, category: "tech", approved: true},
Transaction{id: 3, amount: 80.0, category: "food", approved: false},
Transaction{id: 4, amount: 300.0, category: "tech", approved: true},
Transaction{id: 5, amount: 120.0, category: "travel", approved: true},
]
// Pipeline: filter → map → filter → map → reduce
approved_only := transactions.filter(fn(t Transaction) bool {
return t.approved
})
large_approved := approved_only.filter(fn(t Transaction) bool {
return is_large(t.amount)
})
with_fees := large_approved.map(fn(t Transaction) f64 {
return t.amount + processing_fee(t.amount)
})
// Build summaries using composition
summaries := large_approved.map(fn(t Transaction) string {
fee := processing_fee(t.amount)
final_amount := t.amount + fee
return format_summary(t.id, final_amount)
})
println(summaries) // ["TX-2: 255.00", "TX-4: 306.00", "TX-5: 122.40"]
// Reduce: compute total processed amount
total_processed := with_fees.reduce(fn(acc f64, amount f64) f64 {
return acc + amount
}, 0.0)
println("Total processed: ${total_processed:.2f}") // 683.40
// Any / All checks
all_approved := transactions.all(fn(t Transaction) bool { return t.approved })
println("All approved? ${all_approved}") // false
has_food := transactions.any(fn(t Transaction) bool { return t.category == "food" })
println("Has food transactions? ${has_food}") // true
}
This example demonstrates how functional style produces code where each step is a clear, testable transformation. There are no mutable accumulators, no index variables to track, and no side effects leaking between operations. The pipeline reads top-to-bottom as a description of what happens to the data.
Conclusion
Functional programming in V is not about rigidly adhering to a paradigm — it's about leveraging the language's strengths to write clearer, safer, and more maintainable code. V's default immutability, first-class function support, and expressive array methods give you a solid foundation for functional design. By keeping functions pure, composing small transformations into pipelines, and isolating side effects at the boundaries, you end up with code that is easier to test, trivial to parallelize, and pleasant to read months later when you return to it. The best practice is pragmatic: reach for FP patterns as your first instinct, but don't hesitate to use mutable state when the situation genuinely demands it. V gives you both tools, and knowing when to apply each is the mark of a seasoned developer.