← Back to DevBytes

Functional Programming in V

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

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:

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

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles