Migration Guide.
Migrate from fp-go v1 to v2 with confidence. Understand breaking changes, choose your strategy, and execute a smooth transition.
Should you migrate?
✅ Reasons to Migrate to v2
New Features:
- Result type - Recommended over Either for error handling
- Effect type - Combines Reader + IO + Result
- Idiomatic packages - 2-32x faster performance
- Better type inference - Improved type parameter ordering
- Generic type aliases - Cleaner type definitions
Improvements:
- More intuitive API
- Better documentation
- Active development
- Future-proof
Requirements:
- Go 1.24+ (uses new generic features)
⚠️ Reasons to Stay on v1
Valid reasons:
- Stuck on Go 1.18-1.23
- Large existing v1 codebase
- Need Writer monad (v1 only, not in v2)
- Team bandwidth constraints
Note: v1 is in maintenance mode but still supported.
Migration overview.
The 5 Breaking Changes
v2 introduces 5 breaking changes that require code updates:
- Generic Type Aliases -
type X = Yinstead oftype X Y - Type Parameter Reordering - Non-inferrable parameters first
- Pair Operates on Second Element - v1 was first, v2 is second (Haskell-aligned)
- Compose is Right-to-Left - Mathematical composition order
- No generic/ Subpackages - Removed internal generic packages
Impact: Most code requires only import path changes. Some code needs minor adjustments.
Quick Migration Checklist
- Upgrade to Go 1.24+required
- Review breaking changesrequired
- Plan migration strategyrequired
- Set up testing environmentrequired
- Update dependenciesoptional
- Update importsoptional
- Fix breaking changesoptional
- Test thoroughlyoptional
Migration strategies.
Strategy 1: Big Bang (Small Codebases)
Best for:
- Small codebases (
<10klines) - Few fp-go usages
- Can afford downtime
Steps:
- Update all imports at once
- Fix breaking changes
- Test everything
- Deploy
Pros:
- ✅ Clean, no mixed versions
- ✅ Fast migration
- ✅ Simple
Cons:
- ❌ Risky for large codebases
- ❌ All-or-nothing
Strategy 2: Gradual Migration (Recommended)
Best for:
- Large codebases
- Production systems
- Risk-averse teams
Steps:
- Run v1 and v2 side-by-side
- Migrate module by module
- Test each module
- Remove v1 when done
Pros:
- ✅ Low risk
- ✅ Incremental testing
- ✅ Can pause/resume
Cons:
- ⚠️ Longer timeline
- ⚠️ Mixed versions temporarily
import (
v1either "github.com/IBM/fp-go/either"
v2result "github.com/IBM/fp-go/v2/result"
)
// Old code uses v1
func oldFunction() v1either.Either[error, int] {
// ...
}
// New code uses v2
func newFunction() v2result.Result[int] {
// ...
}
// Bridge between versions
func bridge() v2result.Result[int] {
v1Result := oldFunction()
return v1either.Fold(
func(err error) v2result.Result[int] {
return v2result.Err[int](err)
},
func(val int) v2result.Result[int] {
return v2result.Ok(val)
},
)(v1Result)
}Strategy 3: New Code Only
Best for:
- Maintaining legacy code
- Limited resources
- Long-term migration
Steps:
- Keep v1 for existing code
- Use v2 for all new code
- Gradually refactor when touching old code
- Eventually remove v1
Pros:
- ✅ Minimal disruption
- ✅ Natural migration
- ✅ Low risk
Cons:
- ⚠️ Very long timeline
- ⚠️ Mixed versions indefinitely
Breaking change details.
1. Generic Type Aliases
What Changed:
v2 uses generic type aliases (type X = Y) instead of type definitions (type X Y).
Why: Go 1.24 added support for generic type aliases, allowing cleaner type definitions.
// v1 - type definition type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
// v2 - type alias type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
Action Required:
- ✅ None for most users
- ⚠️ Update custom type definitions if you created them
2. Type Parameter Reordering
What Changed: Type parameters that cannot be inferred are now first.
Why: Better type inference. Go can infer trailing type parameters but not leading ones.
// v1 func Map[A, B any](f func(A) B) func(Either[error, A]) Either[error, B]
// v2 func Map[B, A any](f func(A) B) func(Either[error, A]) Either[error, B] // ^ ^ // | Can be inferred from function argument // Cannot be inferred, so comes first
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. More intuitive for most use cases.
// 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")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 = 125. No generic/ Subpackages
What Changed:
Removed generic/ subpackages from all modules.
Why: Generic type aliases make them unnecessary. Cleaner API.
// v1 - generic subpackage import "github.com/IBM/fp-go/ioeither/generic"
// v2 - no generic subpackage import "github.com/IBM/fp-go/v2/ioeither"
Common migration patterns.
Pattern 1: Either → Result
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)
}Pattern 2: IOEither → IOResult
// 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)
}
}