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

The Zen of Go.

Balance functional programming with Go's philosophy. Learn when to use fp-go patterns and when standard Go is better.

// Philosophy
fp-go complements Go, doesn't replace it.
// When to use
Complex errors, optional values, data transforms.
// When not to
Simple ops, hot paths, team unfamiliar.
01

Go's philosophy.

Go values:

  1. Simplicity - Easy to read and understand
  2. Clarity - Explicit over implicit
  3. Pragmatism - Practical over theoretical
  4. Composition - Small pieces that work together
  5. Concurrency - Built-in support for concurrent programming

fp-go embraces these values.

02

fp-go is Go-first.

Not a Replacement

Before
replace-all.go
// ❌ Don't replace all Go code with fp-go
func processUser(id string) result.Result[User] {
  return function.Pipe10(
      step1, step2, step3, step4, step5,
      step6, step7, step8, step9, step10,
  )
}
After
use-where-valuable.go
// ✅ Use fp-go where it adds value
func processUser(id string) (User, error) {
  user, err := fetchUser(id)
  if err != nil {
      return User{}, err
  }
  
  // Use fp-go for complex transformations
  validated := result.Chain(validateUser)(result.Ok(user))
  
  return validated.Fold(
      func(err error) (User, error) { return User{}, err },
      func(u User) (User, error) { return u, nil },
  )
}

Complement, Don't Replace

complement.go
// Standard Go for simple cases
func add(a, b int) int {
  return a + b
}

// fp-go for complex error handling
func processOrder(id string) result.Result[Order] {
  return function.Pipe3(
      fetchOrder(id),
      result.Chain(validateOrder),
      result.Chain(enrichOrder),
  )
}
03

When to use fp-go.

✅ Use fp-go When:

1. Complex Error Handling

Before
repetitive-errors.go
func processData(input string) (Output, error) {
  parsed, err := parse(input)
  if err != nil {
      return Output{}, err
  }
  
  validated, err := validate(parsed)
  if err != nil {
      return Output{}, err
  }
  
  transformed, err := transform(validated)
  if err != nil {
      return Output{}, err
  }
  
  return transformed, nil
}

// Repetitive error checking
After
clean-pipeline.go
func processData(input string) result.Result[Output] {
  return function.Pipe3(
      parse(input),
      result.Chain(validate),
      result.Chain(transform),
  )
}

// Clean and composable

2. Optional Values

Before
nil-checks.go
func findUser(id string) *User {
  // nil means not found
  return db.FindByID(id)
}

// Lots of nil checks
user := findUser("123")
if user != nil {
  email := user.Email
  if email != "" {
      // Use email
  }
}
After
option-chain.go
func findUser(id string) option.Option[User] {
  user := db.FindByID(id)
  if user == nil {
      return option.None[User]()
  }
  return option.Some(*user)
}

// Chain operations
email := function.Pipe2(
  findUser("123"),
  option.Chain(getEmail),
)

3. Data Transformations

Before
imperative-transform.go
func processUsers(users []User) []UserDTO {
  result := make([]UserDTO, 0, len(users))
  for _, user := range users {
      if user.Active {
          dto := UserDTO{
              ID:   user.ID,
              Name: strings.ToUpper(user.Name),
          }
          result = append(result, dto)
      }
  }
  return result
}
After
functional-transform.go
import "github.com/IBM/fp-go/v2/array"

func processUsers(users []User) []UserDTO {
  return function.Pipe2(
      array.Filter(func(u User) bool { return u.Active }),
      array.Map(toDTO),
  )(users)
}

func toDTO(u User) UserDTO {
  return UserDTO{
      ID:   u.ID,
      Name: strings.ToUpper(u.Name),
  }
}

4. Composable Pipelines

pipelines.go
// Build reusable pipelines
var processUser = function.Flow3(
  normalize,
  validate,
  enrich,
)

// Use in different contexts
user1 := processUser(rawUser1)
user2 := processUser(rawUser2)

❌ Don't Use fp-go When:

1. Simple Operations

Before
overkill.go
// ❌ Overkill
result := result.Map(func(x int) int {
  return x + 1
})(result.Ok(5))
After
simple-go.go
// ✅ Just use standard Go
x := 5 + 1

2. Performance-Critical Code

Before
hot-path-fp.go
// ❌ Hot path with millions of calls
for i := 0; i < 1000000; i++ {
  result := option.Map(transform)(opt)
}
After
hot-path-go.go
// ✅ Use standard Go for hot paths
for i := 0; i < 1000000; i++ {
  if opt.IsSome() {
      value := transform(opt.Value())
  }
}

3. Team Unfamiliar with FP

Before
confusing.go
// ❌ Confusing for team
var pipeline = function.Flow5(
  step1, step2, step3, step4, step5,
)
After
gradual.go
// ✅ Start simple, introduce gradually
func process(input Input) Output {
  step1Result := step1(input)
  step2Result := step2(step1Result)
  // ...
  return step5Result
}

4. Standard Go is Clearer

Before
forced-fp.go
// ❌ Forced FP
result := option.Fold(
  func() string { return "" },
  func(s string) string { return s },
)(opt)
After
clear-go.go
// ✅ Clear Go
var result string
if opt.IsSome() {
  result = opt.Value()
}
04

Balancing FP and Go idioms.

Pattern 1: FP Core, Go Boundaries

fp-core-go-boundaries.go
// Pure FP core
func processOrder(order Order) result.Result[Order] {
  return function.Pipe3(
      result.Ok(order),
      result.Chain(validateOrder),
      result.Chain(enrichOrder),
  )
}

// Go-style API
func ProcessOrder(order Order) (Order, error) {
  result := processOrder(order)
  return result.Fold(
      func(err error) (Order, error) { return Order{}, err },
      func(o Order) (Order, error) { return o, nil },
  )
}

Pattern 2: Gradual Adoption

gradual-adoption.go
// Phase 1: Start with error handling
func fetchUser(id string) result.Result[User] {
  user, err := db.FindByID(id)
  return result.FromGoError(user, err)
}

// Phase 2: Add composition
func processUser(id string) result.Result[User] {
  return function.Pipe2(
      fetchUser(id),
      result.Chain(validateUser),
  )
}

// Phase 3: Full pipeline
func processUser(id string) result.Result[UserDTO] {
  return function.Pipe4(
      fetchUser(id),
      result.Chain(validateUser),
      result.Chain(enrichUser),
      result.Map(toDTO),
  )
}

Pattern 3: Hybrid Approach

hybrid.go
// Mix FP and standard Go
func processUsers(users []User) ([]UserDTO, error) {
  // Use fp-go for transformation
  active := array.Filter(func(u User) bool {
      return u.Active
  })(users)
  
  // Standard Go for complex logic
  var dtos []UserDTO
  for _, user := range active {
      // Complex validation
      if err := complexValidation(user); err != nil {
          return nil, err
      }
      
      // Use fp-go for transformation
      dto := toDTO(user)
      dtos = append(dtos, dto)
  }
  
  return dtos, nil
}
05

Go idioms with fp-go.

Idiom 1: Errors are Values

errors-as-values.go
// Go idiom: return errors
func fetchUser(id string) (User, error) {
  // ...
}

// fp-go: errors in Result
func fetchUser(id string) result.Result[User] {
  // ...
}

// Both are valid - choose based on context

Idiom 2: Accept Interfaces, Return Structs

interfaces.go
// Go idiom
type UserService interface {
  GetUser(id string) (User, error)
}

type userService struct {
  db Database
}

func (s *userService) GetUser(id string) (User, error) {
  // Can use fp-go internally
  result := function.Pipe2(
      s.fetchUser(id),
      result.Chain(s.validateUser),
  )
  
  // Return Go-style
  return result.Fold(
      func(err error) (User, error) { return User{}, err },
      func(u User) (User, error) { return u, nil },
  )
}

Idiom 3: Make Zero Values Useful

zero-values.go
// Go idiom: zero values work
type Config struct {
  Port    int    // 0 is valid
  Host    string // "" is valid
  Timeout time.Duration // 0 is valid
}

// fp-go: use Option for truly optional values
type Config struct {
  Port    int
  Host    string
  Timeout option.Option[time.Duration] // None means use default
}

Idiom 4: Keep Packages Focused

focused-packages.go
// Go idiom: small, focused packages
package user

// Don't mix everything
// ❌ user/fp.go, user/imperative.go

// ✅ Use fp-go where it helps
func (s *Service) GetUser(id string) (User, error) {
  // Use fp-go internally
  result := s.fetchAndValidate(id)
  
  // Return Go-style
  return result.Fold(
      func(err error) (User, error) { return User{}, err },
      func(u User) (User, error) { return u, nil },
  )
}
06

Testing with fp-go.

Test Pure Functions

test-pure.go
// Pure functions are easy to test
func TestNormalizeUser(t *testing.T) {
  user := User{Name: "  JOHN  "}
  normalized := normalizeUser(user)
  
  assert.Equal(t, "john", normalized.Name)
}

Test Pipelines

test-pipelines.go
// Test the pipeline structure
func TestProcessUser(t *testing.T) {
  // Mock dependencies
  fetchUser = func(id string) result.Result[User] {
      return result.Ok(User{ID: id})
  }
  
  // Test pipeline
  result := processUser("123")
  
  assert.True(t, result.IsOk())
}

Test with Table Tests (Go Idiom)

table-tests.go
func TestValidateUser(t *testing.T) {
  tests := []struct {
      name    string
      user    User
      wantErr bool
  }{
      {"valid user", User{Email: "test@example.com"}, false},
      {"invalid email", User{Email: "invalid"}, true},
      {"empty email", User{Email: ""}, true},
  }
  
  for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) {
          result := validateUser(tt.user)
          
          if tt.wantErr {
              assert.True(t, result.IsErr())
          } else {
              assert.True(t, result.IsOk())
          }
      })
  }
}
07

Performance considerations.

Measure First

benchmark.go
// Don't assume - benchmark
func BenchmarkStandard(b *testing.B) {
  for i := 0; i < b.N; i++ {
      _ = standardApproach()
  }
}

func BenchmarkFPGo(b *testing.B) {
  for i := 0; i < b.N; i++ {
      _ = fpgoApproach()
  }
}

// Then decide based on results

Use Idiomatic Packages

idiomatic.go
// fp-go provides idiomatic (faster) versions
import "github.com/IBM/fp-go/v2/array/idiomatic"

// 2-32x faster for array operations
filtered := idiomatic.Filter(predicate)(array)

Optimize Hot Paths

optimize-hot-paths.go
// Cold path: use fp-go for clarity
func processConfig(config Config) result.Result[Config] {
  return function.Pipe3(
      result.Ok(config),
      result.Chain(validate),
      result.Chain(enrich),
  )
}

// Hot path: optimize
func processRequest(req Request) Response {
  // Direct implementation for performance
  if !isValid(req) {
      return errorResponse
  }
  return successResponse
}
08

Team adoption.

Start Small

start-small.go
// Phase 1: Use Result for error handling
func fetchUser(id string) result.Result[User] {
  user, err := db.FindByID(id)
  return result.FromGoError(user, err)
}

// Phase 2: Add Option for optional values
func findConfig(key string) option.Option[string] {
  // ...
}

// Phase 3: Introduce composition
func processUser(id string) result.Result[User] {
  return function.Pipe2(
      fetchUser(id),
      result.Chain(validateUser),
  )
}

Provide Training

training.go
// Create examples for your team
// examples/user_processing.go

// Example 1: Simple Result usage
func example1() {
  result := fetchUser("123")
  result.Fold(
      func(err error) { fmt.Println("Error:", err) },
      func(user User) { fmt.Println("User:", user) },
  )
}

// Example 2: Chaining operations
func example2() {
  result := function.Pipe2(
      fetchUser("123"),
      result.Chain(validateUser),
  )
}

Set Guidelines

Document when to use fp-go:

Use fp-go for:

  • Complex error handling (3+ sequential operations)
  • Optional values (instead of nil pointers)
  • Data transformations (map, filter, reduce)

Use standard Go for:

  • Simple operations
  • Performance-critical code
  • Public APIs (convert at boundary)