Skip to main content
Version: v2.2.82 (latest)
Migration · 02 / 03

v1 to v2 Migration.

Complete step-by-step guide for migrating from fp-go v1 to v2. Detailed examples, solutions, and testing strategies for each breaking change.

// Prerequisites
Go 1.24+, backup code, review usage.
// Main work
Update imports, fix 5 breaking changes.
// Testing
Unit, integration, performance tests.
01

Prerequisites checklist.

Before starting migration:

1. Upgrade Go Version

check-go.sh
# Check current version
go version

# Must be 1.24 or higher
# If not, upgrade Go first

Why: v2 requires Go 1.24+ for generic type alias support.

2. Backup Your Code

backup.sh
# Create a migration branch
git checkout -b migrate-to-fp-go-v2

# Or tag current state
git tag pre-fp-go-v2-migration

3. Review Current Usage

review.sh
# Find all fp-go imports
grep -r "github.com/IBM/fp-go" . --include="*.go"

# Count usages
grep -r "github.com/IBM/fp-go" . --include="*.go" | wc -l
02

Step 1: Update dependencies.

Add v2 Dependency

add-v2.sh
# Add v2 (keeps v1 if already present)
go get github.com/IBM/fp-go/v2

# Update go.mod
go mod tidy

Your go.mod should now have:

go.mod
require (
  github.com/IBM/fp-go v1.x.x        // Optional: keep for gradual migration
  github.com/IBM/fp-go/v2 v2.x.x     // New v2 dependency
)

Remove v1 (Optional)

Only after full migration is complete.

remove-v1.sh
# Only after full migration
go get github.com/IBM/fp-go@none
go mod tidy
03

Step 2: Update imports.

Create a script to update imports:

migrate-imports.sh
#!/bin/bash
# migrate-imports.sh

# Update all .go files
find . -name "*.go" -type f -exec sed -i '' \
's|github.com/IBM/fp-go/|github.com/IBM/fp-go/v2/|g' {} +

echo "Import paths updated. Run 'go build' to check for issues."

Run it:

run-script.sh
chmod +x migrate-imports.sh
./migrate-imports.sh

Manual Approach

Update each import:

Before
v1-imports.go
// Before (v1)
import (
  "github.com/IBM/fp-go/either"
  "github.com/IBM/fp-go/option"
  "github.com/IBM/fp-go/ioeither"
)
After
v2-imports.go
// After (v2)
import (
  "github.com/IBM/fp-go/v2/either"
  "github.com/IBM/fp-go/v2/option"
  "github.com/IBM/fp-go/v2/ioeither"
)

Gradual Migration (v1 + v2)

Use import aliases:

aliases.go
import (
  v1either "github.com/IBM/fp-go/either"
  v2either "github.com/IBM/fp-go/v2/either"
  
  v1option "github.com/IBM/fp-go/option"
  v2option "github.com/IBM/fp-go/v2/option"
)
04

Step 3: Fix breaking changes.

Breaking Change 1: Generic Type Aliases

What Changed:

Before
v1-type-def.go
// v1 - type definition
type IOEither[E, A any] func() E.Either[E, A]
After
v2-type-alias.go
// v2 - type alias
type IOEither[E, A any] = func() E.Either[E, A]

Impact: Mostly internal. Your code likely works without changes.

Action Required:

If you defined custom types based on fp-go types:

Before
custom-v1.go
// v1 - Update this
type MyEither[E, A any] either.Either[E, A]
After
custom-v2.go
// v2 - To this
type MyEither[E, A any] = either.Either[E, A]

Breaking Change 2: Type Parameter Reordering

What Changed:

Type parameters that cannot be inferred are now first.

Before
v1-params.go
// v1 signature
func Map[A, B any](f func(A) B) func(Either[error, A]) Either[error, B]
After
v2-params.go
// v2 signature
func Map[B, A any](f func(A) B) func(Either[error, A]) Either[error, B]
//       ^  ^
//       |  Inferred from function argument
//       Cannot be inferred, so comes first

Impact: Most code works without changes due to type inference.

Migration Pattern:

v1-explicit.go
// v1 - explicit types
result := either.Chain[User, UserProfile](func(u User) either.Either[error, UserProfile] {
  return fetchProfile(u.ID)
})(userEither)

Action Required:

  • ✅ Remove explicit type parameters (let Go infer)
  • ⚠️ If you must specify types, reverse the order

Breaking Change 3: Pair Operates on Second Element

What Changed:

Pair operations now target the second element instead of the first.

Why: Aligns with Haskell and other FP languages.

Before
v1-pair.go
// v1 - operates on FIRST element
pair := pair.MakePair(1, "hello")
mapped := pair.Map(func(x int) int { 
  return x * 2 
})(pair)
// Result: Pair(2, "hello")
After
v2-pair.go
// v2 - operates on SECOND element
pair := pair.MakePair(1, "hello")
mapped := pair.Map(func(s string) string { 
  return strings.ToUpper(s) 
})(pair)
// Result: Pair(1, "HELLO")

Action Required:

  • ⚠️ Review all Pair usage
  • ⚠️ Update Map, Chain, etc. to target second element
  • ⚠️ Or swap pair elements if needed

Breaking Change 4: Compose is Right-to-Left

What Changed:

Compose now applies functions right-to-left (mathematical composition).

Why: Aligns with mathematical notation: (f ∘ g)(x) = f(g(x))

v1-compose.go
composed := function.Compose2(
  func(x int) int { return x + 1 },  // Applied first
  func(x int) int { return x * 2 },  // Applied second
)
result := composed(5) // (5 + 1) * 2 = 12

Action Required:

  • ⚠️ Reverse function order in Compose calls
  • ✅ Or switch to Flow (left-to-right, unchanged)

Breaking Change 5: No generic/ Subpackages

What Changed:

Removed generic/ subpackages from all modules.

Why: Generic type aliases make them unnecessary.

Before
v1-generic.go
// v1 - generic subpackage
import "github.com/IBM/fp-go/ioeither/generic"
After
v2-no-generic.go
// v2 - no generic subpackage
import "github.com/IBM/fp-go/v2/ioeither"

Action Required:

  • ⚠️ Remove /generic from import paths
  • ⚠️ Update function calls if needed
05

Step 4: Adopt v2 best practices.

Use Result Instead of Either

Recommended: Use Result for error handling in v2.

Before
v1-either.go
// v1 - Either
func divide(a, b int) either.Either[error, int] {
  if b == 0 {
      return either.Left[int](errors.New("division by zero"))
  }
  return either.Right[error](a / b)
}
After
v2-result.go
// v2 - Result (recommended)
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)
}

Use IOResult Instead of IOEither

Before
v1-ioeither.go
// v1 - IOEither
func readFile(path string) ioeither.IOEither[error, []byte] {
  return func() either.Either[error, []byte] {
      data, err := os.ReadFile(path)
      if err != nil {
          return either.Left[[]byte](err)
      }
      return either.Right[error](data)
  }
}
After
v2-ioresult.go
// v2 - IOResult (recommended)
func readFile(path string) ioresult.IOResult[[]byte] {
  return func() result.Result[[]byte] {
      data, err := os.ReadFile(path)
      return result.FromGoError(data, err)
  }
}

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)
mapped := idiomatic.Map(transform)(filtered)
06

Step 5: Test thoroughly.

Unit Tests

unit-test.go
func TestMigration(t *testing.T) {
  // Test v2 behavior
  result := divide(10, 2)
  
  assert.True(t, result.IsOk())
  assert.Equal(t, 5, result.GetOrElse(func() int { return 0 }))
  
  // Test error case
  errorResult := divide(10, 0)
  assert.True(t, errorResult.IsErr())
}

Integration Tests

integration-test.go
func TestEndToEnd(t *testing.T) {
  // Test full pipeline with v2
  result := function.Pipe3(
      fetchData(),
      result.Chain(processData),
      result.Chain(saveData),
  )
  
  assert.True(t, result.IsOk())
}

Performance Tests

benchmark.go
func BenchmarkV1(b *testing.B) {
  for i := 0; i < b.N; i++ {
      _ = v1Function()
  }
}

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

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

Common migration issues.

Issue 1: Type Inference Fails

Problem:

inference-fail.go
// Compiler can't infer types
result := either.Map(transform)(myEither)
// Error: cannot infer type parameters

Solution:

inference-fix.go
// Specify types explicitly
result := either.Map[OutputType, InputType](transform)(myEither)

// Or use type annotation
var result either.Either[error, OutputType] = either.Map(transform)(myEither)

Issue 2: Pair Behavior Changed

Problem:

pair-problem.go
// v1 code that operated on first element
mapped := pair.Map(func(x int) int { return x * 2 })(myPair)

Solution:

pair-fix.go
// v2: Update to operate on second element
mapped := pair.Map(func(s string) string { return strings.ToUpper(s) })(myPair)

// Or swap the pair elements
swapped := pair.Swap(myPair)
mapped := pair.Map(func(x int) int { return x * 2 })(swapped)

Issue 3: Compose Order Reversed

Problem:

compose-problem.go
// v1 code with left-to-right composition
composed := function.Compose2(step1, step2)

Solution:

compose-fix.go
// v2: Reverse order
composed := function.Compose2(step2, step1)

// Or use Flow (unchanged)
pipeline := function.Flow2(step1, step2)

Issue 4: Generic Import Not Found

Problem:

generic-problem.go
// v1 code with generic subpackage
import "github.com/IBM/fp-go/ioeither/generic"

Solution:

generic-fix.go
// v2: Remove /generic
import "github.com/IBM/fp-go/v2/ioeither"

Issue 5: Performance Regression

Problem: Performance slower after migration.

Solution:

performance-fix.go
// Use idiomatic packages for better performance
import "github.com/IBM/fp-go/v2/array/idiomatic"

// 2-32x faster
result := idiomatic.Map(transform)(array)
08

Verification checklist.

Steps
  • All imports updated to v2required
  • Breaking changes fixedrequired
  • All tests passingrequired
  • Code compiles without errorsrequired
  • Performance benchmarks runoptional
  • Documentation updatedoptional
  • Team trained on v2 changesoptional
  • Rollback plan documentedoptional