Documentation
¶
Overview ¶
Package idiomatic provides functional programming constructs optimized for idiomatic Go.
Overview ¶
The idiomatic package reimagines functional programming patterns using Go's native tuple return values instead of wrapper structs. This approach provides better performance, lower memory overhead, and a more familiar API for Go developers while maintaining functional programming principles.
Key Differences from Standard Packages ¶
Unlike the standard fp-go packages (option, either, result) which use struct wrappers, the idiomatic package uses Go's native tuple patterns:
Standard either: Either[E, A] (struct wrapper) Idiomatic result: (A, error) (native Go tuple) Standard option: Option[A] (struct wrapper) Idiomatic option: (A, bool) (native Go tuple)
Performance Benefits ¶
The idiomatic approach offers several performance advantages:
- Zero allocation for creating values (no heap allocations)
- Better CPU cache locality (no pointer indirection)
- Native Go compiler optimizations for tuples
- Reduced garbage collection pressure
- Smaller memory footprint
Benchmarks show 2-10x performance improvements for common operations compared to struct-based implementations, especially for simple operations like Map, Chain, and Fold.
Design Philosophy ¶
The idiomatic packages follow these design principles:
1. Native Go Idioms: Use Go's built-in patterns (tuples, error handling) 2. Zero-Cost Abstraction: No runtime overhead for functional patterns 3. Composability: All operations compose naturally with standard Go code 4. Familiarity: API feels natural to Go developers 5. Type Safety: Full compile-time type checking
Subpackages ¶
The idiomatic package includes three main subpackages:
## idiomatic/option
Implements the Option monad using (value, bool) tuples where the boolean indicates presence (true) or absence (false). This is similar to Go's map lookup pattern.
Example usage:
import "github.com/IBM/fp-go/v2/idiomatic/option"
// Creating options
some := option.Some(42) // (42, true)
none := option.None[int]() // (0, false)
// Transforming values
double := option.Map(N.Mul(2))
result := double(some) // (84, true)
result = double(none) // (0, false)
// Chaining operations
validate := option.Chain(func(x int) (int, bool) {
if x > 0 { return x * 2, true }
return 0, false
})
result = validate(some) // (84, true)
// Pattern matching
value := option.GetOrElse(func() int { return 0 })(some) // 42
## idiomatic/result
Implements the Either/Result monad using (value, error) tuples, leveraging Go's standard error handling pattern. By convention, (value, nil) represents success and (zero, error) represents failure.
Example usage:
import "github.com/IBM/fp-go/v2/idiomatic/result"
// Creating results
success := result.Right(42) // (42, nil)
failure := result.Left[int](errors.New("oops")) // (0, error)
// Transforming values
double := result.Map(N.Mul(2))
res := double(success) // (84, nil)
res = double(failure) // (0, error)
// Chaining operations (short-circuits on error)
validate := result.Chain(func(x int) (int, error) {
if x > 0 { return x * 2, nil }
return 0, errors.New("negative")
})
res = validate(success) // (84, nil)
// Pattern matching
output := result.Fold(
func(err error) string { return "Error: " + err.Error() },
func(n int) string { return fmt.Sprintf("Success: %d", n) },
)(success) // "Success: 42"
// Direct integration with Go error handling
value, err := result.Right(42)
if err != nil {
// handle error
}
## idiomatic/ioresult
Implements the IOResult monad using func() (value, error) for IO operations that can fail. This combines IO effects (side-effectful operations) with Go's standard error handling pattern. It's the idiomatic version of IOEither, representing computations that perform side effects and may fail.
Example usage:
import "github.com/IBM/fp-go/v2/idiomatic/ioresult"
// Creating IOResult values
success := ioresult.Of(42) // func() (int, error) returning (42, nil)
failure := ioresult.Left[int](errors.New("oops")) // func() (int, error) returning (0, error)
// Reading a file with IOResult
readConfig := ioresult.FromIO(func() string {
return "config.json"
})
// Transforming IO operations
processFile := F.Pipe2(
readConfig,
ioresult.Map(strings.ToUpper),
ioresult.Chain(func(path string) ioresult.IOResult[[]byte] {
return func() ([]byte, error) {
return os.ReadFile(path)
}
}),
)
// Execute the IO operation
content, err := processFile()
if err != nil {
log.Fatal(err)
}
// Resource management with Bracket
result, err := ioresult.Bracket(
func() (*os.File, error) { return os.Open("data.txt") },
func(f *os.File, err error) ioresult.IOResult[any] {
return func() (any, error) { return nil, f.Close() }
},
func(f *os.File) ioresult.IOResult[[]byte] {
return func() ([]byte, error) { return io.ReadAll(f) }
},
)()
Key features:
- Lazy evaluation: Operations are not executed until the IOResult is called
- Composable: Chain IO operations that may fail
- Error handling: Automatic error propagation and recovery
- Resource safety: Bracket ensures proper resource cleanup
- Parallel execution: ApPar and TraverseArrayPar for concurrent operations
Type Signatures ¶
The idiomatic packages use function types that work naturally with Go tuples:
For option package:
Operator[A, B any] = func(A, bool) (B, bool) // Transform Option[A] to Option[B] Kleisli[A, B any] = func(A) (B, bool) // Monadic function from A to Option[B]
For result package:
Operator[A, B any] = func(A, error) (B, error) // Transform Result[A] to Result[B] Kleisli[A, B any] = func(A) (B, error) // Monadic function from A to Result[B]
For ioresult package:
IOResult[A any] = func() (A, error) // IO operation returning A or error Operator[A, B any] = func(IOResult[A]) IOResult[B] // Transform IOResult[A] to IOResult[B] Kleisli[A, B any] = func(A) IOResult[B] // Monadic function from A to IOResult[B]
When to Use Idiomatic vs Standard Packages ¶
Use idiomatic packages when:
- Performance is critical (hot paths, tight loops)
- You want zero-allocation functional patterns
- You prefer Go's native error handling style
- You're integrating with existing Go code that uses tuples
- Memory efficiency matters (embedded systems, high-scale services)
- You need IO operations with error handling (use ioresult)
Use standard packages when:
- You need full algebraic data type semantics
- You're porting code from other FP languages
- You want explicit Either[E, A] with custom error types
- You need the complete suite of FP abstractions
- Code clarity outweighs performance concerns
Choosing Between result and ioresult ¶
Use result when:
- Operations are pure (same input always produces same output)
- No side effects are involved (no IO, no state mutation)
- You want to represent success/failure without execution delay
Use ioresult when:
- Operations perform IO (file system, network, database)
- Side effects are part of the computation
- You need lazy evaluation (defer execution until needed)
- You want to compose IO operations that may fail
- Resource management is required (files, connections, locks)
Performance Comparison ¶
Benchmark results comparing idiomatic vs standard packages (examples):
Operation Standard Idiomatic Improvement --------- -------- --------- ----------- Right/Some 3.2 ns/op 0.5 ns/op 6.4x faster Left/None 3.5 ns/op 0.5 ns/op 7.0x faster Map (Right/Some) 5.8 ns/op 1.2 ns/op 4.8x faster Map (Left/None) 3.8 ns/op 1.0 ns/op 3.8x faster Chain (success) 8.2 ns/op 2.1 ns/op 3.9x faster Fold 6.5 ns/op 1.8 ns/op 3.6x faster
Memory allocations:
Operation Standard Idiomatic --------- -------- --------- Right/Some 16 B/op 0 B/op Map 16 B/op 0 B/op Chain 32 B/op 0 B/op
Interoperability ¶
The idiomatic packages provide conversion functions for working with standard packages:
// Converting between idiomatic.option and standard option
import (
stdOption "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/idiomatic/option"
)
// Standard to idiomatic (conceptually - check actual API)
stdOpt := stdOption.Some(42)
idiomaticOpt := stdOption.Unwrap(stdOpt) // Returns (42, true)
// Idiomatic to standard (conceptually - check actual API)
value, ok := option.Some(42)
stdOpt = stdOption.FromTuple(value, ok)
// Converting between idiomatic.result and standard result
import (
stdResult "github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/idiomatic/result"
)
// The conversion is straightforward with Unwrap/UnwrapError
stdRes := stdResult.Right[error](42)
value, err := stdResult.UnwrapError(stdRes) // (42, nil)
// And back
stdRes = stdResult.TryCatchError(value, err)
Common Patterns ¶
## Pipeline Composition
Build complex data transformations using function composition:
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/result"
)
output, err := F.Pipe3(
parseInput(input),
result.Map(validate),
result.Chain(process),
result.Map(format),
)
## IO Pipeline with IOResult
Compose IO operations that may fail:
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
)
// Define IO operations
readFile := func(path string) ioresult.IOResult[[]byte] {
return func() ([]byte, error) {
return os.ReadFile(path)
}
}
parseJSON := func(data []byte) ioresult.IOResult[Config] {
return func() (Config, error) {
var cfg Config
err := json.Unmarshal(data, &cfg)
return cfg, err
}
}
// Compose operations (not executed yet)
loadConfig := F.Pipe1(
readFile("config.json"),
ioresult.Chain(parseJSON),
ioresult.Map(validateConfig),
)
// Execute the IO pipeline
config, err := loadConfig()
if err != nil {
log.Fatal(err)
}
## Error Accumulation with Validation
The idiomatic/result package supports validation patterns for accumulating multiple errors:
import "github.com/IBM/fp-go/v2/idiomatic/result"
results := []error{
validate1(input),
validate2(input),
validate3(input),
}
allErrors := result.ValidationErrors(results)
## Working with Collections
Transform arrays while handling errors or missing values:
import "github.com/IBM/fp-go/v2/idiomatic/option"
// Transform array, short-circuit on first None
input := []int{1, 2, 3}
output, ok := option.TraverseArray(func(x int) (int, bool) {
if x > 0 { return x * 2, true }
return 0, false
})(input) // ([2, 4, 6], true)
import "github.com/IBM/fp-go/v2/idiomatic/result"
// Transform array, short-circuit on first error
output, err := result.TraverseArray(func(x int) (int, error) {
if x > 0 { return x * 2, nil }
return 0, errors.New("invalid")
})(input) // ([2, 4, 6], nil)
Integration with Standard Library ¶
The idiomatic packages integrate seamlessly with Go's standard library:
// File operations with result
readFile := result.Chain(func(path string) ([]byte, error) {
return os.ReadFile(path)
})
content, err := readFile("config.json", nil)
// HTTP requests with result
import "github.com/IBM/fp-go/v2/idiomatic/result/http"
resp, err := http.MakeRequest(http.GET, "https://api.example.com/data")
// Database queries with option
findUser := func(id int) (User, bool) {
user, err := db.QueryRow("SELECT * FROM users WHERE id = ?", id)
if err != nil {
return User{}, false
}
return user, true
}
// File operations with IOResult and resource safety
import "github.com/IBM/fp-go/v2/idiomatic/ioresult"
processFile := ioresult.Bracket(
// Acquire resource
func() (*os.File, error) {
return os.Open("data.txt")
},
// Release resource (always called)
func(f *os.File, err error) ioresult.IOResult[any] {
return func() (any, error) {
return nil, f.Close()
}
},
// Use resource
func(f *os.File) ioresult.IOResult[string] {
return func() (string, error) {
data, err := io.ReadAll(f)
return string(data), err
}
},
)
content, err := processFile()
// System command execution with IOResult
import ioexec "github.com/IBM/fp-go/v2/idiomatic/ioresult/exec"
version := F.Pipe1(
ioexec.Command("git")([]string{"version"})([]byte{}),
ioresult.Map(func(output exec.CommandOutput) string {
return string(exec.StdOut(output))
}),
)
result, err := version()
Best Practices ¶
1. Use descriptive error messages:
result.Left[User](fmt.Errorf("user %d not found", id))
2. Prefer composition over complex logic:
F.Pipe3(input,
result.Map(step1),
result.Chain(step2),
result.Map(step3),
)
3. Use Fold for final value extraction:
output := result.Fold(
func(err error) Response { return ErrorResponse(err) },
func(data Data) Response { return SuccessResponse(data) },
)(result)
4. Leverage GetOrElse for defaults:
value := option.GetOrElse(func() Config { return defaultConfig })(maybeConfig)
5. Use FromPredicate for validation:
positiveInt := result.FromPredicate(
func(x int) bool { return x > 0 },
func(x int) error { return fmt.Errorf("%d is not positive", x) },
)
Testing ¶
Testing code using idiomatic packages is straightforward:
func TestTransformation(t *testing.T) {
input := 21
result, err := F.Pipe2(
input,
result.Right[int],
result.Map(N.Mul(2)),
)
assert.NoError(t, err)
assert.Equal(t, 42, result)
}
func TestOptionHandling(t *testing.T) {
value, ok := F.Pipe2(
42,
option.Some[int],
option.Map(N.Mul(2)),
)
assert.True(t, ok)
assert.Equal(t, 84, value)
}
Resources ¶
For more information on functional programming patterns in Go:
- fp-go documentation: https://github.com/IBM/fp-go
- Standard option package: github.com/IBM/fp-go/v2/option
- Standard either package: github.com/IBM/fp-go/v2/either
- Standard result package: github.com/IBM/fp-go/v2/result
- Standard ioeither package: github.com/IBM/fp-go/v2/ioeither
See the subpackage documentation for detailed API references:
- idiomatic/option: Option monad using (value, bool) tuples
- idiomatic/result: Result/Either monad using (value, error) tuples
- idiomatic/ioresult: IOResult monad using func() (value, error) for IO operations
Directories
¶
| Path | Synopsis |
|---|---|
|
context
|
|
|
readerresult
Package readerresult provides a ReaderResult monad specialized for context.Context.
|
Package readerresult provides a ReaderResult monad specialized for context.Context. |
|
Package ioresult provides functional programming combinators for working with IO operations that can fail with errors, following Go's idiomatic (value, error) tuple pattern.
|
Package ioresult provides functional programming combinators for working with IO operations that can fail with errors, following Go's idiomatic (value, error) tuple pattern. |
|
exec
Package exec provides utilities for executing system commands with IOResult-based error handling.
|
Package exec provides utilities for executing system commands with IOResult-based error handling. |
|
Package option implements the Option monad using idiomatic Go tuple signatures.
|
Package option implements the Option monad using idiomatic Go tuple signatures. |
|
number
Package number provides Option-based utilities for number conversions.
|
Package number provides Option-based utilities for number conversions. |
|
Package readerioresult provides a ReaderIOResult monad that combines Reader, IO, and Result monads.
|
Package readerioresult provides a ReaderIOResult monad that combines Reader, IO, and Result monads. |
|
Package readerresult provides a ReaderResult monad that combines the Reader and Result monads.
|
Package readerresult provides a ReaderResult monad that combines the Reader and Result monads. |
|
Package result provides an idiomatic Go approach to error handling using the (value, error) tuple pattern.
|
Package result provides an idiomatic Go approach to error handling using the (value, error) tuple pattern. |
|
exec
Package exec provides utilities for executing system commands with Either-based error handling.
|
Package exec provides utilities for executing system commands with Either-based error handling. |
|
http
Package http provides utilities for creating HTTP requests with Either-based error handling.
|
Package http provides utilities for creating HTTP requests with Either-based error handling. |