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

Composition.

Combine simple, reusable functions into pipelines. The essence of functional programming, applied to Go.

// Tool of choice
Flow — left-to-right pipelines.
// Building block
Small, pure functions.
// Win
Reusable, testable, easy to modify.
01

What is composition?

Composition is combining simple functions to create more complex ones. Think of it like LEGO blocks — each block is simple, blocks connect in standard ways, complex structures emerge from simple pieces.

lego.gotested
// Simple functions (LEGO blocks)
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 (build something)
result := square(addOne(double(5)))  // ((5*2)+1)^2 = 121
02

Why composition matters.

monolithic.go
// Monolithic function - hard to reuse
func processUserData(data string) string {
  // Parse
  parsed := strings.TrimSpace(data)

  // Validate
  if len(parsed) == 0 {
      return ""
  }

  // Transform
  lower := strings.ToLower(parsed)

  // Format
  return fmt.Sprintf("user_%s", lower)
}

// Can't reuse individual steps
// Hard to test each step
// Hard to modify
03

Composition patterns.

Pattern 1 — Manual composition

nested.go
// Right-to-left (inside-out)
result := square(addOne(double(5)))
//         ^      ^      ^
//         3rd    2nd    1st

// Execution order: double → addOne → square
// (5*2) = 10
// (10+1) = 11
// (11*11) = 121
Prossimple

No dependencies.

Just function calls.

Conswatch out

Hard to read with many functions.

Pattern 2 — Compose (right-to-left)

compose.gotested
import "github.com/IBM/fp-go/v2/function"

// Mathematical composition: (f ∘ g)(x) = f(g(x))
composed := function.Compose3(
  square,   // Applied LAST (3rd)
  addOne,   // Applied 2nd
  double,   // Applied FIRST (1st)
)

result := composed(5)  // 121

// Execution: double(5) → addOne(10) → square(11)
When to reach for Compose.

Mathematical, concise. But right-to-left can confuse Go readers — prefer Flow unless you specifically want the mathematical convention.

Pattern 3 — Flow (left-to-right) ⭐

flow.gotested
import "github.com/IBM/fp-go/v2/function"

// Pipeline style: data flows left to right
pipeline := function.Flow3(
  double,   // Applied FIRST (1st)
  addOne,   // Applied 2nd
  square,   // Applied LAST (3rd)
)

result := pipeline(5)  // 121

// Execution: double(5) → addOne(10) → square(11)
Recommended.

Intuitive, reads like a pipeline. This is the default choice.

Pattern 4 — Pipe (data-first) ⭐

pipe.gotested
import "github.com/IBM/fp-go/v2/function"

// Start with data, pipe through functions
result := function.Pipe3(
  5,        // Start with data
  double,   // 10
  addOne,   // 11
  square,   // 121
)
When to use Pipe.

Very clear, data-first. Best for one-off processing where you don't need to reuse the pipeline.

04

fp-go composition functions.

Create reusable pipelines.

flow-signatures.go
// Flow2 - 2 functions
func Flow2[A, B, C any](
  f func(A) B,
  g func(B) C,
) func(A) C

// Flow3 - 3 functions
func Flow3[A, B, C, D any](
  f func(A) B,
  g func(B) C,
  h func(C) D,
) func(A) D

// Up to Flow9
flow-example.gotested
// Create pipeline
processNumber := function.Flow4(
  func(x int) int { return x * 2 },      // double
  func(x int) int { return x + 1 },      // add one
  func(x int) int { return x * x },      // square
  func(x int) string { return fmt.Sprintf("Result: %d", x) },
)

// Use it
output := processNumber(5)  // "Result: 121"

Pipe — data-first

Process data through a pipeline.

pipe-signatures.go
// Pipe2 - data + 2 functions
func Pipe2[A, B, C any](
  a A,
  f func(A) B,
  g func(B) C,
) C

// Pipe3 - data + 3 functions
func Pipe3[A, B, C, D any](
  a A,
  f func(A) B,
  g func(B) C,
  h func(C) D,
) D

// Up to Pipe9
pipe-example.gotested
result := function.Pipe4(
  "  HELLO WORLD  ",
  strings.TrimSpace,
  strings.ToLower,
  func(s string) string { return strings.ReplaceAll(s, " ", "_") },
  func(s string) string { return "slug_" + s },
)
// "slug_hello_world"

Compose — mathematical

Right-to-left composition.

compose-signatures.go
// Compose2 - 2 functions (right-to-left)
func Compose2[A, B, C any](
  f func(B) C,  // Applied SECOND
  g func(A) B,  // Applied FIRST
) func(A) C

// Example
composed := function.Compose2(
  square,   // Applied second
  double,   // Applied first
)
result := composed(5)  // square(double(5)) = 100
Reminder.

Use Flow instead for better Go readability.

05

Real-world examples.

String processing

without.go
func processUsername(input string) string {
  // Step 1: trim
  trimmed := strings.TrimSpace(input)

  // Step 2: lowercase
  lower := strings.ToLower(trimmed)

  // Step 3: remove special chars
  cleaned := regexp.MustCompile(`[^a-z0-9]`).ReplaceAllString(lower, "")

  // Step 4: limit length
  if len(cleaned) > 20 {
      cleaned = cleaned[:20]
  }

  return cleaned
}

Data transformation

without.go
func processOrders(orders []Order) []OrderSummary {
  // Filter active orders
  var active []Order
  for _, order := range orders {
      if order.Status == "active" {
          active = append(active, order)
      }
  }

  // Calculate totals
  var withTotals []Order
  for _, order := range active {
      total := 0.0
      for _, item := range order.Items {
          total += item.Price
      }
      order.Total = total
      withTotals = append(withTotals, order)
  }

  // Convert to summaries
  var summaries []OrderSummary
  for _, order := range withTotals {
      summaries = append(summaries, OrderSummary{
          ID:    order.ID,
          Total: order.Total,
      })
  }

  return summaries
}

API response processing

without.go
func processAPIResponse(data []byte) (Result, error) {
  // Parse JSON
  var raw RawResponse
  if err := json.Unmarshal(data, &raw); err != nil {
      return Result{}, err
  }

  // Validate
  if raw.Status != "success" {
      return Result{}, errors.New("invalid status")
  }

  // Transform
  result := Result{
      ID:   raw.Data.ID,
      Name: strings.ToUpper(raw.Data.Name),
  }

  return result, nil
}
06

Composition with monads.

Compose effectful functions by lifting them with Map / Chain.

Chaining operations

chain.gotested
import (
  "github.com/IBM/fp-go/v2/result"
  "github.com/IBM/fp-go/v2/function"
)

// Each function returns Result
func fetchUser(id string) result.Result[User] { /* ... */ }
func validateUser(user User) result.Result[User] { /* ... */ }
func enrichUser(user User) result.Result[User] { /* ... */ }
func saveUser(user User) result.Result[User] { /* ... */ }

// Compose with Chain
var processUser = function.Flow3(
  fetchUser,
  result.Chain(validateUser),
  result.Chain(enrichUser),
  result.Chain(saveUser),
)

// Usage
res := processUser("user-123")
// Stops at first error, or returns final success

Mixing Map and Chain

mix.go
// Map: transform the value
// Chain: transform and return new Result

var pipeline = function.Flow4(
  fetchUser,                           // Result[User]
  result.Map(normalizeUser),           // Result[User] - just transform
  result.Chain(validateUser),          // Result[User] - can fail
  result.Map(toDTO),                   // Result[UserDTO] - just transform
)
07

Point-free style.

Define functions without naming their arguments.

With points (arguments)

with-points.go
// Mentions 'x' explicitly
double := func(x int) int {
  return x * 2
}

// Mentions 'users' explicitly
activeUsers := func(users []User) []User {
  return array.Filter(func(u User) bool {
      return u.Active
  })(users)
}

Point-free

point-free.go
// No mention of arguments
var double = function.Flow1(func(x int) int { return x * 2 })

// No mention of 'users'
var activeUsers = array.Filter(func(u User) bool {
  return u.Active
})

// Use it
result := activeUsers(users)
Use point-free whencleaner

The pipeline is clearer without naming intermediate values.

clear.go
var processUsers = function.Flow3(
  array.Filter(isActive),
  array.Map(normalize),
  array.Map(toDTO),
)
Don't force whenharder to read

Logic is complex — a normal function is clearer.

unclear.go
var processUser = func(user User) User {
  // Complex logic here
  return user
}
08

Best practices.

1 — Keep functions small

Afterrecommended
good.go
func trim(s string) string { return strings.TrimSpace(s) }
func lower(s string) string { return strings.ToLower(s) }
func addPrefix(p string) func(string) string {
  return func(s string) string { return p + s }
}
Beforeavoid
bad.go
func processString(s string, prefix string, maxLen int) string {
  s = strings.TrimSpace(s)
  s = strings.ToLower(s)
  s = prefix + s
  if len(s) > maxLen {
      s = s[:maxLen]
  }
  return s
}

2 — Use descriptive names

Afterclear
good.go
var processUsername = function.Flow3(
  trim,
  lower,
  removeSpecialChars,
)
Beforeunclear
bad.go
var process = function.Flow3(
  f1,
  f2,
  f3,
)

3 — Limit pipeline length

limit.go
// ✅ Good: 3-5 steps
var pipeline = function.Flow4(
  parse,
  validate,
  transform,
  format,
)

// ⚠️ Consider breaking up: 10+ steps
var hugePipeline = function.Flow10(
  step1, step2, step3, step4, step5,
  step6, step7, step8, step9, step10,
)

// Better: break into sub-pipelines
var preprocess = function.Flow3(step1, step2, step3)
var process = function.Flow3(step4, step5, step6)
var postprocess = function.Flow4(step7, step8, step9, step10)

var pipeline = function.Flow3(preprocess, process, postprocess)

4 — Test each function

testing.gotested
// Test individual functions
func TestTrim(t *testing.T) {
  assert.Equal(t, "hello", trim("  hello  "))
}

func TestLower(t *testing.T) {
  assert.Equal(t, "hello", lower("HELLO"))
}

// Test composition
func TestProcessUsername(t *testing.T) {
  assert.Equal(t, "user_john", processUsername("  JOHN  "))
}

5 — Use type aliases for clarity

aliases.go
// Define types for clarity
type Username string
type Email string
type UserID string

// Functions with clear types
func normalizeUsername(s string) Username {
  return Username(strings.ToLower(strings.TrimSpace(s)))
}

func validateEmail(s string) result.Result[Email] {
  if !strings.Contains(s, "@") {
      return result.Err[Email](errors.New("invalid email"))
  }
  return result.Ok(Email(s))
}
09

Performance considerations.

Composition overhead

overhead.go
// Minimal overhead
composed := function.Flow3(f1, f2, f3)
result := composed(input)

// Equivalent to:
result := f3(f2(f1(input)))

// Just function calls - very fast

When to optimize

optimize.go
// ✅ Fine for most cases
var process = function.Flow5(
  step1, step2, step3, step4, step5,
)

// ⚠️ Hot path with millions of calls?
// Consider inlining:
func process(x int) int {
  x = step1(x)
  x = step2(x)
  x = step3(x)
  x = step4(x)
  return step5(x)
}
bench.go
func BenchmarkComposed(b *testing.B) {
  composed := function.Flow3(double, addOne, square)
  for i := 0; i < b.N; i++ {
      _ = composed(5)
  }
}

func BenchmarkInlined(b *testing.B) {
  for i := 0; i < b.N; i++ {
      x := 5
      x = double(x)
      x = addOne(x)
      _ = square(x)
  }
}

// Results: negligible difference for most cases
10

Common questions.

Isn't this just function calls?

Yes. Composition is a structured way to call functions. The value is in reusability, testability, clarity, and maintainability.

When should I use composition?

Use it when you have a sequence of transformations, the steps are reusable, and you want clear/testable code. Don't force it when logic is complex and branching, steps are tightly coupled, or it makes the code less clear.

Flow vs. Pipe vs. Compose — quick rule.
  • Flow — create reusable pipelines.
  • Pipe — one-off data processing.
  • Compose — only if you prefer mathematical style.

Most people prefer Flow.

11

Summary

Composition5 / 5 complete
  • Build complex from simple
  • Reusable functions
  • Clear data flow
  • Easy to test
  • Easy to modify
ToolDirectionUse for
FlowLeft-to-right
Reusable pipelines (recommended).
PipeData-first
One-off processing of a known value.
ComposeRight-to-left
Mathematical convention.
Key takeaway.

Composition is about building maintainable systems from simple, reusable pieces. Use it where it adds clarity, not complexity.