Migration · 03 / 03
v1 and v2 Interop.
Run fp-go v1 and v2 side-by-side during gradual migration. Learn patterns for bridging between versions safely and efficiently.
// Key pattern
Convert at boundaries, not throughout.
// Bridge package
Isolate all conversion logic.
// Feature flags
Control which version to use.
01
Why run both versions?
Use Cases
Gradual Migration:
- Migrate module by module
- Test incrementally
- Minimize risk
Legacy Support:
- Keep old code working
- Add new features with v2
- Eventual migration
Team Coordination:
- Different teams migrate at different speeds
- Shared libraries need both versions
- Smooth transition
02
Setup guide.
1. Install Both Versions
install.sh
# Install v1 and v2 go get github.com/IBM/fp-go go get github.com/IBM/fp-go/v2 # Verify go.mod cat go.mod
Your go.mod should have:
go.mod
module myapp go 1.24 require ( github.com/IBM/fp-go v1.x.x github.com/IBM/fp-go/v2 v2.x.x )
2. Use Import Aliases
imports.go
import ( // v1 imports with v1 prefix v1either "github.com/IBM/fp-go/either" v1option "github.com/IBM/fp-go/option" v1ioeither "github.com/IBM/fp-go/ioeither" // v2 imports with v2 prefix v2either "github.com/IBM/fp-go/v2/either" v2result "github.com/IBM/fp-go/v2/result" v2ioresult "github.com/IBM/fp-go/v2/ioresult" )
3. Organize Code
structure.txt
myapp/ ├── legacy/ # v1 code │ ├── user.go │ └── auth.go ├── new/ # v2 code │ ├── api.go │ └── service.go └── bridge/ # Interop code └── convert.go
03
Conversion patterns.
Pattern 1: Either v1 → Result v2
Convert v1 Either to v2 Result:
either-to-result.go
// Conversion function
func EitherToResult[A any](e v1either.Either[error, A]) v2result.Result[A] {
return v1either.Fold(
// Left (error) → Err
func(err error) v2result.Result[A] {
return v2result.Err[A](err)
},
// Right (value) → Ok
func(val A) v2result.Result[A] {
return v2result.Ok(val)
},
)(e)
}
// Usage
func legacyFunction() v1either.Either[error, User] {
// v1 code
return v1either.Right[error](User{ID: "123"})
}
func newFunction() v2result.Result[User] {
v1Result := legacyFunction()
return EitherToResult(v1Result)
}Pattern 2: Result v2 → Either v1
Convert v2 Result to v1 Either:
result-to-either.go
// Conversion function using Fold
func ResultToEither[A any](r v2result.Result[A]) v1either.Either[error, A] {
return r.Fold(
// Err → Left
func(err error) v1either.Either[error, A] {
return v1either.Left[A](err)
},
// Ok → Right
func(val A) v1either.Either[error, A] {
return v1either.Right[error](val)
},
)
}
// Usage
func newFunction() v2result.Result[User] {
// v2 code
return v2result.Ok(User{ID: "123"})
}
func legacyFunction() v1either.Either[error, User] {
v2Result := newFunction()
return ResultToEither(v2Result)
}Pattern 3: Option v1 ↔ Option v2
Options are similar in both versions:
option-conversion.go
// v1 Option → v2 Option
func OptionV1ToV2[A any](opt v1option.Option[A]) v2option.Option[A] {
return v1option.Fold(
func() v2option.Option[A] {
return v2option.None[A]()
},
func(val A) v2option.Option[A] {
return v2option.Some(val)
},
)(opt)
}
// v2 Option → v1 Option
func OptionV2ToV1[A any](opt v2option.Option[A]) v1option.Option[A] {
return v2option.Fold(
func() v1option.Option[A] {
return v1option.None[A]()
},
func(val A) v1option.Option[A] {
return v1option.Some(val)
},
)(opt)
}Pattern 4: IOEither v1 → IOResult v2
ioeither-to-ioresult.go
// Conversion function
func IOEitherToIOResult[A any](
ioe v1ioeither.IOEither[error, A],
) v2ioresult.IOResult[A] {
return func() v2result.Result[A] {
// Execute v1 IOEither
e := ioe()
// Convert Either to Result
return EitherToResult(e)
}
}
// Usage
func legacyReadFile(path string) v1ioeither.IOEither[error, []byte] {
return func() v1either.Either[error, []byte] {
data, err := os.ReadFile(path)
if err != nil {
return v1either.Left[[]byte](err)
}
return v1either.Right[error](data)
}
}
func newReadFile(path string) v2ioresult.IOResult[[]byte] {
v1IO := legacyReadFile(path)
return IOEitherToIOResult(v1IO)
}Pattern 5: IOResult v2 → IOEither v1
ioresult-to-ioeither.go
// Conversion function
func IOResultToIOEither[A any](
ior v2ioresult.IOResult[A],
) v1ioeither.IOEither[error, A] {
return func() v1either.Either[error, A] {
// Execute v2 IOResult
r := ior()
// Convert Result to Either
return ResultToEither(r)
}
}
// Usage
func newFetchData(url string) v2ioresult.IOResult[Data] {
return func() v2result.Result[Data] {
// v2 implementation
return v2result.Ok(Data{})
}
}
func legacyFetchData(url string) v1ioeither.IOEither[error, Data] {
v2IO := newFetchData(url)
return IOResultToIOEither(v2IO)
}04
Real-world examples.
Example 1: HTTP Handler Bridge
http-bridge.go
// Legacy v1 service
type LegacyUserService struct{}
func (s *LegacyUserService) GetUser(id string) v1ioeither.IOEither[error, User] {
return func() v1either.Either[error, User] {
// v1 implementation
user := User{ID: id, Name: "John"}
return v1either.Right[error](user)
}
}
// New v2 service
type UserService struct {
legacy *LegacyUserService
}
func (s *UserService) GetUser(id string) v2ioresult.IOResult[User] {
// Bridge to legacy service
v1Result := s.legacy.GetUser(id)
return IOEitherToIOResult(v1Result)
}
// HTTP handler using v2
func HandleGetUser(service *UserService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
// Use v2 API
result := service.GetUser(id)()
result.Fold(
func(err error) {
http.Error(w, err.Error(), http.StatusInternalServerError)
},
func(user User) {
json.NewEncoder(w).Encode(user)
},
)
}
}Example 2: Database Layer Migration
database-bridge.go
// Legacy v1 repository
type LegacyUserRepo struct {
db *sql.DB
}
func (r *LegacyUserRepo) FindByID(id string) v1ioeither.IOEither[error, User] {
return func() v1either.Either[error, User] {
var user User
err := r.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
if err != nil {
return v1either.Left[User](err)
}
return v1either.Right[error](user)
}
}
// New v2 repository
type UserRepo struct {
db *sql.DB
}
func (r *UserRepo) FindByID(id string) v2ioresult.IOResult[User] {
return func() v2result.Result[User] {
var user User
err := r.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
return v2result.FromGoError(user, err)
}
}
// Bridge service using both
type UserService struct {
legacyRepo *LegacyUserRepo
newRepo *UserRepo
useV2 bool // Feature flag
}
func (s *UserService) GetUser(id string) v2ioresult.IOResult[User] {
if s.useV2 {
return s.newRepo.FindByID(id)
}
// Bridge to v1
v1Result := s.legacyRepo.FindByID(id)
return IOEitherToIOResult(v1Result)
}Example 3: Shared Library
shared-library.go
// shared/types.go - Common types
package shared
type User struct {
ID string
Name string
Email string
}
// shared/v1/user.go - v1 API
package v1
import (
v1either "github.com/IBM/fp-go/either"
"myapp/shared"
)
func ValidateUser(user shared.User) v1either.Either[error, shared.User] {
// v1 implementation
return v1either.Right[error](user)
}
// shared/v2/user.go - v2 API
package v2
import (
v2result "github.com/IBM/fp-go/v2/result"
"myapp/shared"
)
func ValidateUser(user shared.User) v2result.Result[shared.User] {
// v2 implementation
return v2result.Ok(user)
}
// shared/bridge/user.go - Conversion
package bridge
import (
v1either "github.com/IBM/fp-go/either"
v2result "github.com/IBM/fp-go/v2/result"
"myapp/shared"
sharedv1 "myapp/shared/v1"
sharedv2 "myapp/shared/v2"
)
// Use v1 from v2 code
func ValidateUserV1AsV2(user shared.User) v2result.Result[shared.User] {
v1Result := sharedv1.ValidateUser(user)
return EitherToResult(v1Result)
}
// Use v2 from v1 code
func ValidateUserV2AsV1(user shared.User) v1either.Either[error, shared.User] {
v2Result := sharedv2.ValidateUser(user)
return ResultToEither(v2Result)
}05
Best practices.
1. Isolate Conversion Logic
Create a dedicated bridge package:
bridge-package.go
// bridge/convert.go
package bridge
// All conversion functions in one place
func EitherToResult[A any](e v1either.Either[error, A]) v2result.Result[A] { ... }
func ResultToEither[A any](r v2result.Result[A]) v1either.Either[error, A] { ... }
func IOEitherToIOResult[A any](ioe v1ioeither.IOEither[error, A]) v2ioresult.IOResult[A] { ... }
// etc.2. Use Feature Flags
Control which version to use:
feature-flags.go
type Config struct {
UseV2UserService bool
UseV2AuthService bool
}
func NewUserService(cfg Config, v1Svc *V1UserService, v2Svc *V2UserService) UserService {
if cfg.UseV2UserService {
return v2Svc
}
return &BridgedUserService{v1: v1Svc}
}3. Test Both Paths
test-both.go
func TestUserService(t *testing.T) {
t.Run("v1 implementation", func(t *testing.T) {
svc := NewUserService(Config{UseV2UserService: false}, v1Svc, v2Svc)
// Test v1 path
})
t.Run("v2 implementation", func(t *testing.T) {
svc := NewUserService(Config{UseV2UserService: true}, v1Svc, v2Svc)
// Test v2 path
})
t.Run("bridge conversion", func(t *testing.T) {
// Test conversion functions
})
}4. Document Version Usage
documentation.go
// Package user provides user management. // // This package is in transition from fp-go v1 to v2. // Current status: // - GetUser: v2 ✅ // - CreateUser: v1 (migrating) // - UpdateUser: v1 (migrating) // - DeleteUser: v1 (not started) // // See MIGRATION.md for details. package user
5. Monitor Performance
benchmark.go
func BenchmarkBridge(b *testing.B) {
b.Run("v1 direct", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = v1Function()
}
})
b.Run("v2 direct", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = v2Function()
}
})
b.Run("v1 via bridge", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = IOResultToIOEither(v2Function())
}
})
}06
Common pitfalls.
Pitfall 1: Nested Conversions
Before
nested-conversions.go
// Converting back and forth v2Result := EitherToResult(v1Either) v1Either2 := ResultToEither(v2Result) v2Result2 := EitherToResult(v1Either2) // Performance overhead!
After
boundary-conversion.go
// Convert once at boundaries
func processData(input Data) v2result.Result[Data] {
// Do all processing in v2
return v2result.Ok(input)
}
// Convert only when interfacing with v1 code
func legacyAPI(input Data) v1either.Either[error, Data] {
v2Result := processData(input)
return ResultToEither(v2Result) // Convert once
}Pitfall 2: Forgetting Error Context
Before
lost-error.go
func ResultToEither[A any](r v2result.Result[A]) v1either.Either[error, A] {
if r.IsOk() {
return v1either.Right[error](r.GetOrElse(func() A { var zero A; return zero }))
}
// Lost the actual error!
return v1either.Left[A](errors.New("error"))
}After
preserve-error.go
func ResultToEither[A any](r v2result.Result[A]) v1either.Either[error, A] {
return r.Fold(
func(err error) v1either.Either[error, A] {
return v1either.Left[A](err) // Preserve error
},
func(val A) v1either.Either[error, A] {
return v1either.Right[error](val)
},
)
}Pitfall 3: Type Inference Issues
Before
inference-fail.go
// Compiler can't infer types result := EitherToResult(v1Either) // Error: cannot infer A
After
explicit-types.go
// Specify type explicitly result := EitherToResult[User](v1Either) // Or use type annotation var result v2result.Result[User] = EitherToResult(v1Either)