Skip to main content
Version: v2.2.82 (latest)
Reference · FAQ

Frequently asked questions.

Common questions about fp-go, functional programming in Go, and when to use these patterns.

01

Getting started.

What is fp-go?

fp-go is a comprehensive functional programming library for Go providing monadic types (Result, Either, Option, IO, Reader, …) and utilities for composing operations, handling errors, and managing side effects in a type-safe, functional way.

  • Type-safe error handling with Result/Either
  • Automatic error propagation through pipelines
  • IO monad for managing side effects
  • Reader monad for dependency injection
  • Comprehensive collection operations
  • Full composition utilities (Pipe, Flow, Compose)
Do I need to know FP to use fp-go?

Short answer: not initially, but learning FP concepts will help you use it effectively.

Start with Result/Either for error handling. Learn Pipe for composition. Gradually explore Map, Chain, and the rest.

Resources: 5-Minute Quickstart, Core Concepts, Pure Functions, Monads Explained.

Which version: v1 or v2?
Use v2recommended

You're on Go 1.24+.

Starting a new project.

Want latest features (Result, Effect, idiomatic packages).

Want better type inference.

Use v1legacy

Stuck on Go 1.18–1.23.

Have existing v1 codebase.

Need Writer monad (v1 only).

Recommendation: use v2 for all new projects. See the migration guide for upgrading.

Is fp-go production-ready?

Yes. fp-go is used in production at IBM (creator and maintainer) and other companies. v2 is stable, actively maintained, and recommended. v1 is stable and in maintenance mode.

02

Performance

What's the performance impact of fp-go?

Standard packages: small overhead from function calls. Typically 5–15% slower than hand-written code. Negligible for most applications. Worth it for type safety and maintainability.

Idiomatic packages (v2 only): 2–32× faster than standard packages. Near-native performance. Use native Go tuples instead of generic types. Recommended for performance-critical code.

Relative cost$go test -bench=. -benchmem
VariantSpeedns/opB/opΔ
Filter — stdlib1.00xbaseline
Filter — fp-go (idiomatic)1.03x+3%
Filter — fp-go (standard)1.14x+14%
Map — fp-go (idiomatic)1.02x+2%
Map — fp-go (standard)1.12x+12%
Reduce — fp-go (idiomatic)1.04x+4%
Reduce — fp-go (standard)1.15x+15%
Bottom line.

Fast enough for 99% of use cases. Use idiomatic packages for hot paths.

When should I use idiomatic packages?
Idiomaticfast path

Performance is critical.

Processing large datasets.

In tight loops.

Hot code paths.

Standarddefault

Type safety matters most.

Performance is adequate.

Code clarity matters most.

Not performance-critical.

example.go
// Standard - type-safe, slightly slower
result := array.Map(func(x int) int { return x * 2 })(data)

// Idiomatic - near-native speed
result := idiomatic.Map(data, func(x int) int { return x * 2 })

See the performance guide for details.

Does fp-go allocate a lot of memory?

No more than idiomatic Go. Result/Either/Option are single allocations per value. Pipelines have intermediate allocations (same as manual code). Idiomatic packages have minimal allocations. For memory-efficient patterns: use iterators for lazy evaluation, use idiomatic packages for large datasets, avoid unnecessary intermediate collections.

03

Error handling.

Should I use Result or Either?
Use Result (v2)recommended

For error handling.

Error type is always error.

Simpler API, more idiomatic for Go.

Use Eitherwhen needed

You need non-error Left values.

Sum types beyond error handling.

Porting from other FP languages.

example.go
// Result - recommended for errors
func divide(a, b int) result.Result[int] {
  if b == 0 {
      return result.Err[int](errors.New("division by zero"))
  }
  return result.Ok(a / b)
}

// Either - for generic sum types
func parseValue(s string) either.Either[ParseError, Value] {
  // Left can be any type, not just error
}
How do I handle multiple errors?

Option 1: Stop at first error (default).

option1.go
result := function.Pipe3(
  step1(),
  result.Chain(step2),
  result.Chain(step3),
)
// Stops at first error

Option 2: Accumulate errors.

option2.go
results := array.TraverseResult(validate)(items)
// Returns Result[[]Item] - all or nothing

// For accumulating all errors, use Validation applicative
// (Advanced pattern - see recipes)

Option 3: Collect errors manually.

option3.go
var errors []error
for _, item := range items {
  if err := validate(item); err != nil {
      errors = append(errors, err)
  }
}
Can I convert between Result and (value, error)?

Yes — use the conversion helpers.

interop.go
// From (value, error) to Result
func fetchUser(id string) result.Result[User] {
  user, err := db.Query(id)
  return result.FromGoError(user, err)
}

// From Result to (value, error)
func legacyAPI() (User, error) {
  result := fetchUser("123")
  return result.ToGoError()
}
04

Usage patterns.

When should I use fp-go vs. idiomatic Go?
Use fp-go forfits

Complex error handling logic.

Data transformation pipelines.

Composable business logic.

Side effect management.

Dependency injection patterns.

Use idiomatic Go forfits

Simple CRUD operations.

Straightforward logic.

Performance-critical hot paths.

When team is unfamiliar with FP.

fp-go-example.go
// Complex pipeline with error handling
func processOrder(order Order) result.Result[Receipt] {
  return function.Pipe5(
      validateOrder(order),
      result.Chain(checkInventory),
      result.Chain(calculatePrice),
      result.Chain(applyDiscounts),
      result.Chain(generateReceipt),
  )
}
idiomatic-example.go
// Simple database query
func getUser(id string) (*User, error) {
  return db.QueryUser(id)
}
Can I mix fp-go with idiomatic Go?

Yes — they work together seamlessly.

mix.go
// Idiomatic Go function
func fetchFromDB(id string) (*User, error) {
  return db.Query(id)
}

// Wrap in Result for fp-go pipeline
func getUser(id string) result.Result[*User] {
  user, err := fetchFromDB(id)
  return result.FromGoError(user, err)
}

// Use in pipeline
result := function.Pipe2(
  getUser("123"),
  result.Map(enrichUser),
  result.Chain(validateUser),
)
How do I handle side effects?

Use IO types.

io.go
// Pure function returning IO
func readFile(path string) io.IO[[]byte] {
  return func() []byte {
      data, _ := os.ReadFile(path)
      return data
  }
}

// Compose IO operations
program := function.Pipe2(
  readFile("config.json"),
  io.Map(parseConfig),
  io.Map(validateConfig),
)

// Execute when ready
config := program() // Side effect happens here

Use IOResult when the effect can fail:

ioresult.go
func readFile(path string) ioresult.IOResult[[]byte] {
  return func() result.Result[[]byte] {
      data, err := os.ReadFile(path)
      return result.FromGoError(data, err)
  }
}

See Effects and IO.

05

Learning & adoption.

PhaseYou learnDifficulty
Week 1Result/Either, Pipe, Map
Low–Medium.
Week 2–4Chain (FlatMap), IO, Reader
Medium.
Month 2+All monadic types, monad laws, optics
Medium–High.
How do I convince my team to use fp-go?
  1. Use for new features only.
  2. Show concrete benefits (fewer bugs, easier testing).
  3. Provide training and examples.
  4. Let the team see the value.

Address concerns: show benchmarks for performance; provide training for the learning curve; start with simple patterns for complexity; do gradual migration for adoption.

Success metrics: reduced bug count, faster development, easier code reviews, better test coverage.

Are there good tutorials or courses?

Official: Quickstart, Core Concepts, Recipes, API docs.

Learning path: Read Why fp-go? → Complete Quickstart → Study Pure Functions → Learn Monads → Practice with Recipes.

06

Comparison

How does fp-go compare to samber/lo?
samber/loutility

Simple collection operations.

Low learning curve.

Excellent performance.

No monadic types, no error handling.

fp-goFP toolkit

Full FP toolkit.

Built-in error handling.

Monadic composition.

Steeper learning curve; idiomatic packages for speed.

both.go
// Use lo for simple operations
filtered := lo.Filter(items, predicate)

// Use fp-go for error handling
result := result.TraverseArray(processWithErrors)(filtered)

See the comparison guide.

Is fp-go similar to fp-ts (TypeScript)?

Yes — fp-go is heavily inspired by fp-ts. Same monadic types, similar API design, same composition patterns, same concepts. Main differences: Go's type system limits (no HKT), different syntax, performance characteristics, ecosystem.

07

Troubleshooting

Why am I getting type inference errors?

Common causes:

1. Type parameters in wrong order.

case1.go
// Wrong - B cannot be inferred
result.Map(func(x int) string { return fmt.Sprintf("%d", x) })

// Right - specify B explicitly
result.Map[string](func(x int) string { return fmt.Sprintf("%d", x) })

2. Missing type parameters.

case2.go
// Wrong
return result.Err(errors.New("error"))

// Right
return result.Err[int](errors.New("error"))

3. Ambiguous types.

case3.go
// Wrong - compiler can't infer
result := result.Ok(nil)

// Right - specify type
result := result.Ok[*User](nil)
How do I debug pipelines?

Use intermediate logging:

debug.go
result := function.Pipe3(
  step1(),
  result.Map(func(x T) T {
      fmt.Printf("After step1: %+v\n", x)
      return x
  }),
  result.Chain(step2),
  result.Map(func(x T) T {
      fmt.Printf("After step2: %+v\n", x)
      return x
  }),
)

Use Fold to inspect:

fold.go
result.Fold(
  func(err error) {
      fmt.Printf("Error: %v\n", err)
  },
  func(val T) {
      fmt.Printf("Success: %+v\n", val)
  },
)

Use the logging package:

logging.go
import "github.com/IBM/fp-go/v2/logging"

result := logging.WithLogging(
  "operation",
  func() result.Result[T] {
      return operation()
  },
)
08

Migration

How do I migrate from v1 to v2?

5 breaking changes:

  1. Generic type aliasestype X = Y instead of type X Y
  2. Type parameter reordering — non-inferrable params first
  3. Pair operates on second — v1 was first, v2 is second
  4. Compose is right-to-left — mathematical composition
  5. No generic/ subpackages — removed

Steps: update imports → fix type-parameter order → update Pair usage → update Compose usage → remove generic/ imports.

See the complete migration guide.

Can I run v1 and v2 side-by-side?

Yes — they can coexist.

side-by-side.go
import (
  v1 "github.com/IBM/fp-go/either"
  v2 "github.com/IBM/fp-go/v2/result"
)

// Use both in same codebase
v1Result := v1.Right[error](42)
v2Result := v2.Ok(42)

See Interop during migration.

09

Contributing

Ways to contribute.
  • Report bugs
  • Suggest features
  • Improve documentation
  • Submit pull requests
  • Help in discussions
  • Star the repository

Read the contributing guide; check the good first issues; join GitHub Discussions.

Where can I get help?