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 checklist.
Before starting migration:
1. Upgrade Go Version
# 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
# 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
# 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
Step 1: Update dependencies.
Add v2 Dependency
# 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:
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.
# Only after full migration go get github.com/IBM/fp-go@none go mod tidy
Step 2: Update imports.
Automated Approach (Recommended)
Create a script to update imports:
#!/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:
chmod +x migrate-imports.sh ./migrate-imports.sh
Manual Approach
Update each import:
// Before (v1) import ( "github.com/IBM/fp-go/either" "github.com/IBM/fp-go/option" "github.com/IBM/fp-go/ioeither" )
// 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:
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" )
Step 3: Fix breaking changes.
Breaking Change 1: Generic Type Aliases
What Changed:
// v1 - type definition type IOEither[E, A any] func() E.Either[E, A]
// 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:
// v1 - Update this type MyEither[E, A any] either.Either[E, A]
// 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.
// v1 signature func Map[A, B any](f func(A) B) func(Either[error, A]) Either[error, B]
// 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 Code
- v2 (Explicit)
- v2 (Inferred)
// v1 - explicit types
result := either.Chain[User, UserProfile](func(u User) either.Either[error, UserProfile] {
return fetchProfile(u.ID)
})(userEither)
// v2 - reordered types
result := either.Chain[UserProfile, User](func(u User) either.Either[error, UserProfile] {
return fetchProfile(u.ID)
})(userEither)
// v2 - inferred (recommended)
result := either.Chain(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.
// 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")
// 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 - Left-to-Right
- v2 - Right-to-Left
- Or Use Flow
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
composed := function.Compose2(
func(x int) int { return x * 2 }, // Applied second
func(x int) int { return x + 1 }, // Applied first
)
result := composed(5) // (5 + 1) * 2 = 12
// Flow is left-to-right, unchanged
pipeline := function.Flow2(
func(x int) int { return x + 1 }, // Applied first
func(x int) int { return x * 2 }, // Applied second
)
result := pipeline(5) // (5 + 1) * 2 = 12Action 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.
// v1 - generic subpackage import "github.com/IBM/fp-go/ioeither/generic"
// v2 - no generic subpackage import "github.com/IBM/fp-go/v2/ioeither"
Action Required:
- ⚠️ Remove
/genericfrom import paths - ⚠️ Update function calls if needed
Step 4: Adopt v2 best practices.
Use Result Instead of Either
Recommended: Use Result for error handling in v2.
// 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)
}
// 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
// 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)
}
}
// 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
// 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)
Step 5: Test thoroughly.
Unit Tests
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
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
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()
}
}Common migration issues.
Issue 1: Type Inference Fails
Problem:
// Compiler can't infer types result := either.Map(transform)(myEither) // Error: cannot infer type parameters
Solution:
// 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:
// v1 code that operated on first element
mapped := pair.Map(func(x int) int { return x * 2 })(myPair)Solution:
// 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:
// v1 code with left-to-right composition composed := function.Compose2(step1, step2)
Solution:
// v2: Reverse order composed := function.Compose2(step2, step1) // Or use Flow (unchanged) pipeline := function.Flow2(step1, step2)
Issue 4: Generic Import Not Found
Problem:
// v1 code with generic subpackage import "github.com/IBM/fp-go/ioeither/generic"
Solution:
// v2: Remove /generic import "github.com/IBM/fp-go/v2/ioeither"
Issue 5: Performance Regression
Problem: Performance slower after migration.
Solution:
// Use idiomatic packages for better performance import "github.com/IBM/fp-go/v2/array/idiomatic" // 2-32x faster result := idiomatic.Map(transform)(array)
Verification checklist.
- 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