12 KiB
fp-go V2: Enhanced Functional Programming for Go 1.24+
fp-go is a comprehensive functional programming library for Go, bringing type-safe functional patterns inspired by fp-ts to the Go ecosystem. Version 2 leverages generic type aliases introduced in Go 1.24, providing a more ergonomic and streamlined API.
📚 Table of Contents
- Overview
- Features
- Requirements
- Installation
- Quick Start
- Breaking Changes
- Key Improvements
- Migration Guide
- What's New
- Documentation
- Contributing
- License
🎯 Overview
fp-go brings the power of functional programming to Go with:
- Type-safe abstractions - Monads, Functors, Applicatives, and more
- Composable operations - Build complex logic from simple, reusable functions
- Error handling - Elegant error management with
Either,Result, andIOEither - Lazy evaluation - Control when and how computations execute
- Optics - Powerful lens, prism, and traversal operations for immutable data manipulation
✨ Features
- 🔒 Type Safety - Leverage Go's generics for compile-time guarantees
- 🧩 Composability - Chain operations naturally with functional composition
- 📦 Rich Type System -
Option,Either,Result,IO,Reader, and more - 🎯 Practical - Designed for real-world Go applications
- 🚀 Performance - Zero-cost abstractions where possible
- 📖 Well-documented - Comprehensive API documentation and examples
- 🧪 Battle-tested - Extensive test coverage
🔧 Requirements
- Go 1.24 or later (for generic type alias support)
📦 Installation
go get github.com/IBM/fp-go/v2
🚀 Quick Start
Working with Option
package main
import (
"fmt"
"github.com/IBM/fp-go/v2/option"
)
func main() {
// Create an Option
some := option.Some(42)
none := option.None[int]()
// Map over values
doubled := option.Map(N.Mul(2))(some)
fmt.Println(option.GetOrElse(0)(doubled)) // Output: 84
// Chain operations
result := option.Chain(func(x int) option.Option[string] {
if x > 0 {
return option.Some(fmt.Sprintf("Positive: %d", x))
}
return option.None[string]()
})(some)
fmt.Println(option.GetOrElse("No value")(result)) // Output: Positive: 42
}
Error Handling with Result
package main
import (
"errors"
"fmt"
"github.com/IBM/fp-go/v2/result"
)
func divide(a, b int) result.Result[int] {
if b == 0 {
return result.Error[int](errors.New("division by zero"))
}
return result.Ok(a / b)
}
func main() {
res := divide(10, 2)
// Pattern match on the result
result.Fold(
func(err error) { fmt.Println("Error:", err) },
func(val int) { fmt.Println("Result:", val) },
)(res)
// Output: Result: 5
// Or use GetOrElse for a default value
value := result.GetOrElse(0)(divide(10, 0))
fmt.Println("Value:", value) // Output: Value: 0
}
Composing IO Operations
package main
import (
"fmt"
"github.com/IBM/fp-go/v2/io"
)
func main() {
// Define pure IO operations
readInput := io.MakeIO(func() string {
return "Hello, fp-go!"
})
// Transform the result
uppercase := io.Map(func(s string) string {
return fmt.Sprintf(">>> %s <<<", s)
})(readInput)
// Execute the IO operation
result := uppercase()
fmt.Println(result) // Output: >>> Hello, fp-go! <<<
}
From V1 to V2
1. Generic Type Aliases
V2 uses generic type aliases which require Go 1.24+. This is the most significant change and enables cleaner type definitions.
V1:
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
V2:
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
2. Generic Type Parameter Ordering
Type parameters that cannot be inferred from function arguments now come first, improving type inference.
V1:
// Ap in V1 - less intuitive ordering
func Ap[R, E, A, B any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
V2:
// Ap in V2 - B comes first as it cannot be inferred
func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
This change allows the Go compiler to infer more types automatically, reducing the need for explicit type parameters.
3. Pair Monad Semantics
Monadic operations for Pair now operate on the second argument to align with the Haskell definition.
V1:
// Operations on first element
pair := MakePair(1, "hello")
result := Map(N.Mul(2))(pair) // Pair(2, "hello")
V2:
// Operations on second element (Haskell-compatible)
pair := MakePair(1, "hello")
result := Map(func(s string) string { return s + "!" })(pair) // Pair(1, "hello!")
4. Endomorphism Compose Semantics
The Compose function for endomorphisms now follows mathematical function composition (right-to-left execution), aligning with standard functional programming conventions.
V1:
// Compose executed left-to-right
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
composed := Compose(double, increment)
result := composed(5) // (5 * 2) + 1 = 11
V2:
// Compose executes RIGHT-TO-LEFT (mathematical composition)
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
composed := Compose(double, increment)
result := composed(5) // (5 + 1) * 2 = 12
// Use MonadChain for LEFT-TO-RIGHT execution
chained := MonadChain(double, increment)
result2 := chained(5) // (5 * 2) + 1 = 11
Key Difference:
Compose(f, g)now meansf ∘ g, which appliesgfirst, thenf(right-to-left)MonadChain(f, g)appliesffirst, theng(left-to-right)
✨ Key Improvements
1. Simplified Type Declarations
Generic type aliases eliminate the need for namespace imports in type declarations.
V1 Approach:
import (
ET "github.com/IBM/fp-go/either"
OPT "github.com/IBM/fp-go/option"
)
func processData(input string) ET.Either[error, OPT.Option[int]] {
// implementation
}
V2 Approach:
import (
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/option"
)
// Define type aliases once
type Result[A any] = result.Result[A]
type Option[A any] = option.Option[A]
// Use them throughout your codebase
func processData(input string) Result[Option[int]] {
// implementation
}
2. No More generic Subpackages
The library implementation no longer requires separate generic subpackages, making the codebase simpler and easier to understand.
V1 Structure:
either/
either.go
generic/
either.go // Generic implementation
V2 Structure:
either/
either.go // Single, clean implementation
3. Better Type Inference
The reordered type parameters allow the Go compiler to infer more types automatically:
V1:
// Often need explicit type parameters
result := Map[Context, error, int, string](transform)(value)
V2:
// Compiler can infer more types
result := Map(transform)(value) // Cleaner!
🚀 Migration Guide
Step 1: Update Go Version
Ensure you're using Go 1.24 or later:
go version # Should show go1.24 or higher
Step 2: Update Import Paths
Change all import paths from github.com/IBM/fp-go to github.com/IBM/fp-go/v2:
Before:
import (
"github.com/IBM/fp-go/either"
"github.com/IBM/fp-go/option"
)
After:
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
)
Step 3: Remove generic Subpackage Imports
If you were using generic subpackages, remove them:
Before:
import (
E "github.com/IBM/fp-go/either/generic"
)
After:
import (
"github.com/IBM/fp-go/v2/either"
)
Step 4: Update Type Parameter Order
Review functions like Ap where type parameter order has changed. The compiler will help identify these:
Before:
result := Ap[Context, error, int, string](value)(funcInContext)
After:
result := Ap[string, Context, error, int](value)(funcInContext)
// Or better yet, let the compiler infer:
result := Ap(value)(funcInContext)
Step 5: Update Pair Operations
If you're using Pair, update operations to work on the second element:
Before (V1):
pair := MakePair(42, "data")
// Map operates on first element
result := Map(N.Mul(2))(pair)
After (V2):
pair := MakePair(42, "data")
// Map operates on second element
result := Map(func(s string) string { return s + "!" })(pair)
Step 6: Simplify Type Aliases
Create project-wide type aliases for common patterns:
// types.go - Define once, use everywhere
package myapp
import (
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/ioresult"
)
type Result[A any] = result.Result[A]
type Option[A any] = option.Option[A]
type IOResult[A any] = ioresult.IOResult[A]
🆕 What's New
Cleaner API Surface
The elimination of generic subpackages means:
- Fewer imports to manage
- Simpler package structure
- Easier to navigate documentation
- More intuitive API
Example: Before and After
V1 Complex Example:
import (
ET "github.com/IBM/fp-go/either"
EG "github.com/IBM/fp-go/either/generic"
IOET "github.com/IBM/fp-go/ioeither"
IOEG "github.com/IBM/fp-go/ioeither/generic"
)
func process() IOET.IOEither[error, string] {
return IOEG.Map[error, int, string](
strconv.Itoa,
)(fetchData())
}
V2 Simplified Example:
import (
"strconv"
"github.com/IBM/fp-go/v2/ioresult"
)
type IOResult[A any] = ioresult.IOResult[A]
func process() IOResult[string] {
return ioresult.Map(
strconv.Itoa,
)(fetchData())
}
📚 Documentation
- API Documentation - Complete API reference
- Code Samples - Practical examples and use cases
- Go 1.24 Release Notes - Information about generic type aliases
Core Modules
- Option - Represent optional values without nil
- Either - Type-safe error handling with left/right values
- Result - Simplified Either with error as left type
- IO - Lazy evaluation and side effect management
- IOEither - Combine IO with error handling
- Reader - Dependency injection pattern
- ReaderIOEither - Combine Reader, IO, and Either for complex workflows
- Array - Functional array operations
- Record - Functional record/map operations
- Optics - Lens, Prism, Optional, and Traversal for immutable updates
🤔 Should I Migrate?
Migrate to V2 if:
- ✅ You can use Go 1.24+
- ✅ You want cleaner, more maintainable code
- ✅ You want better type inference
- ✅ You're starting a new project
Stay on V1 if:
- ⚠️ You're locked to Go < 1.24
- ⚠️ Migration effort outweighs benefits for your project
- ⚠️ You need stability in production (V2 is newer)
🤝 Contributing
Contributions are welcome! Here's how you can help:
- Report bugs - Open an issue with a clear description and reproduction steps
- Suggest features - Share your ideas for improvements
- Submit PRs - Fix bugs or add features (please discuss major changes first)
- Improve docs - Help make the documentation clearer and more comprehensive
Please read our contribution guidelines before submitting pull requests.
🐛 Issues and Feedback
Found a bug or have a suggestion? Please open an issue on GitHub.
📄 License
This project is licensed under the Apache License 2.0. See the LICENSE file for details.
Made with ❤️ by IBM