Skip to main content
Version: v2.2.82 (latest)
Concepts · 01 / 06

Pure functions.

The foundation of functional programming — what makes a function pure, why it matters, and how to apply it in Go.

// Rule 1
Same input → same output.
// Rule 2
No side effects.
// Benefit
Testable, composable, parallelizable.
01

What is a pure function?

A pure function is a function that:

  1. Always returns the same output for the same input (deterministic)
  2. Has no side effects (doesn't modify external state)

That's it. Simple concept, powerful implications.

02

The two rules.

Rule 1 — deterministic

pure.gotested
// Pure: always returns same result for same input
func add(a, b int) int {
  return a + b
}

// Always returns 5
result1 := add(2, 3) // 5
result2 := add(2, 3) // 5
result3 := add(2, 3) // 5

Rule 2 — no side effects

pure.gotested
// Pure: doesn't modify anything outside itself
func multiply(a, b int) int {
  return a * b
}

// No external state changed
result := multiply(3, 4) // 12
03

Common side effects.

Modifying variables

impure-cache.go
var cache map[string]string

// Impure: modifies global cache
func getCached(key string) string {
  if val, ok := cache[key]; ok {
      return val
  }
  val := fetchFromDB(key)
  cache[key] = val  // Side effect!
  return val
}

I/O operations

impure-io.go
// Impure: reads from file system
func readConfig() Config {
  data, _ := os.ReadFile("config.json")  // Side effect!
  var config Config
  json.Unmarshal(data, &config)
  return config
}

// Impure: writes to console
func logMessage(msg string) {
  fmt.Println(msg)  // Side effect!
}

Network calls

impure-net.go
// Impure: makes HTTP request
func fetchUser(id string) User {
  resp, _ := http.Get("https://api.example.com/users/" + id)  // Side effect!
  var user User
  json.NewDecoder(resp.Body).Decode(&user)
  return user
}

Random and time

impure-nondet.go
// Impure: uses random number generator
func generateID() string {
  return fmt.Sprintf("id-%d", rand.Int())  // Side effect!
}

// Impure: depends on current time
func isExpired(expiresAt time.Time) bool {
  return time.Now().After(expiresAt)  // Side effect!
}
04

Why pure functions matter.

1. Easy to test

pure-test.gotested
// Pure function
func calculateDiscount(price float64, percentage float64) float64 {
  return price * (percentage / 100)
}

// Simple test - no setup needed
func TestCalculateDiscount(t *testing.T) {
  result := calculateDiscount(100, 10)
  assert.Equal(t, 10.0, result)
}

2. Easy to reason about

reasoning.go
// Pure: you know exactly what it does
func fullName(first, last string) string {
  return first + " " + last
}

// No need to check:
// - What global variables it uses
// - What files it reads
// - What network calls it makes
// - What it logs
// Just look at the function!

3. Easy to compose

compose.gotested
// Pure functions compose naturally
func double(x int) int { return x * 2 }
func addOne(x int) int { return x + 1 }
func square(x int) int { return x * x }

// Compose them
result := square(addOne(double(5)))  // ((5*2)+1)^2 = 121

// Or with fp-go
import "github.com/IBM/fp-go/v2/function"

composed := function.Flow3(double, addOne, square)
result := composed(5)  // 121

4. Cacheable (memoization)

memo.go
// Pure functions can be safely cached
var cache = make(map[int]int)

func expensiveCalculation(n int) int {
  if result, ok := cache[n]; ok {
      return result  // Return cached result
  }

  // Do expensive calculation
  result := /* ... */
  cache[n] = result
  return result
}

// Safe because function is pure!
// Same input always gives same output

5. Parallelizable

parallel.go
// Pure functions are safe to run in parallel
func processItem(item Item) Result {
  // Pure processing
  return transform(item)
}

// Safe to parallelize
var wg sync.WaitGroup
for _, item := range items {
  wg.Add(1)
  go func(i Item) {
      defer wg.Done()
      result := processItem(i)  // No race conditions!
      results <- result
  }(item)
}
wg.Wait()
05

Making functions pure.

Pattern 1 — pass dependencies as parameters

impure.go
var db *sql.DB

func getUser(id string) (User, error) {
  // Uses global db
  row := db.QueryRow("SELECT * FROM users WHERE id = ?", id)
  var user User
  err := row.Scan(&user.ID, &user.Name)
  return user, err
}

Pattern 2 — return new values, don't modify

impure.go
// Modifies the slice
func addItem(items []Item, item Item) {
  items = append(items, item)  // Modifies input!
}

original := []Item{{ID: 1}}
addItem(original, Item{ID: 2})
// original is now modified

Pattern 3 — separate pure logic from effects

impure.go
// Mixed pure logic and effects
func processOrder(orderID string) error {
  // Effect: fetch from DB
  order, err := db.GetOrder(orderID)
  if err != nil {
      return err
  }

  // Pure: calculate total
  total := 0.0
  for _, item := range order.Items {
      total += item.Price
  }

  // Effect: save to DB
  order.Total = total
  return db.SaveOrder(order)
}

Pattern 4 — use fp-go for effects

standard.go
// Impure: executes immediately
func fetchUser(id string) (User, error) {
  resp, err := http.Get("https://api.example.com/users/" + id)
  if err != nil {
      return User{}, err
  }
  defer resp.Body.Close()

  var user User
  err = json.NewDecoder(resp.Body).Decode(&user)
  return user, err
}
06

Pure functions in Go.

Go is not a pure functional language — and that's fine. Pure functions are a tool, not a religion.

Use pure functions forfits

Business logic.

Calculations.

Transformations.

Validations.

Formatting.

Parsing (when possible).

Don't force purity fortrade-offs

I/O operations (use fp-go types instead).

Logging (use structured logging).

Metrics (use dedicated libraries).

Performance-critical code (if purity hurts performance).

balance.go
// Pure: business logic
func calculateShipping(weight float64, distance float64) float64 {
  baseRate := 5.0
  weightRate := weight * 0.5
  distanceRate := distance * 0.1
  return baseRate + weightRate + distanceRate
}

// Pure: validation
func validateEmail(email string) error {
  if !strings.Contains(email, "@") {
      return errors.New("invalid email")
  }
  return nil
}

// Impure but necessary: I/O
func saveOrder(order Order) error {
  // Use fp-go to make it more manageable
  return ioresult.Of(func() result.Result[Order] {
      // Database operation
      return result.FromGoError(order, db.Save(order))
  })()
}
07

Real-world examples.

E-commerce pricing

impure.go
var taxRate float64
var discountRate float64

func calculatePrice(basePrice float64) float64 {
  price := basePrice
  price -= price * (discountRate / 100)
  price += price * (taxRate / 100)
  return price
}

// Hard to test - depends on global state

Data transformation

impure.go
func processUsers(users []User) {
  for i := range users {
      users[i].Name = strings.ToUpper(users[i].Name)
      users[i].Email = strings.ToLower(users[i].Email)
      users[i].Active = true
  }
}

// Modifies input - surprising behavior

Configuration

impure.go
var config Config

func init() {
  data, _ := os.ReadFile("config.json")
  json.Unmarshal(data, &config)
}

func getTimeout() time.Duration {
  return config.Timeout
}

// Global state, hard to test
08

Testing pure functions.

Simple tests

test_basic.gotested
func TestPureFunctions(t *testing.T) {
  // No setup needed!

  t.Run("add", func(t *testing.T) {
      assert.Equal(t, 5, add(2, 3))
      assert.Equal(t, 0, add(-1, 1))
  })

  t.Run("multiply", func(t *testing.T) {
      assert.Equal(t, 12, multiply(3, 4))
      assert.Equal(t, 0, multiply(0, 100))
  })
}

Property-based testing

test_properties.gotested
func TestAddCommutative(t *testing.T) {
  // Pure functions have mathematical properties
  for i := 0; i < 100; i++ {
      a := rand.Int()
      b := rand.Int()

      // Commutative: a + b = b + a
      assert.Equal(t, add(a, b), add(b, a))
  }
}

func TestAddAssociative(t *testing.T) {
  for i := 0; i < 100; i++ {
      a := rand.Int()
      b := rand.Int()
      c := rand.Int()

      // Associative: (a + b) + c = a + (b + c)
      assert.Equal(t, add(add(a, b), c), add(a, add(b, c)))
  }
}
09

Common questions.

Aren't all Go functions impure?

No. Many Go functions are pure: strings.ToUpper, math.Max, strconv.Itoa, most of encoding/json parsing.

Should I never use global variables?

Use them wisely. Global constants are fine. Global configuration loaded once at startup is often okay. Global mutable state is problematic.

What about logging?

Logging is a side effect — but often acceptable. Prefer structured logging, log at boundaries (not in pure functions), and use context for request-scoped logging.

Is this practical in Go?

Yes. Many successful Go projects use pure functions extensively. It's about balance, not dogma.

10

Summary

Pure functions7 / 7 complete
  • Same input → same output
  • No side effects
  • Easy to test
  • Easy to reason about
  • Easy to compose
  • Cacheable
  • Parallelizable
Key takeaway.

Pure functions are a tool for writing better code. Use them where they help, don't force them where they don't.