mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-09 23:11:40 +02:00
506 lines
16 KiB
Go
506 lines
16 KiB
Go
// Copyright (c) 2025 IBM Corp.
|
|
// All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// 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
|
|
package idiomatic
|