Documentation
¶
Overview ¶
Package ioref provides mutable references in the IO monad.
Overview ¶
IORef represents a mutable reference that can be read and written within IO computations. It provides thread-safe access to shared mutable state using read-write locks, making it safe to use across multiple goroutines.
This package is inspired by Haskell's Data.IORef module and provides a functional approach to managing mutable state with explicit IO effects.
Core Operations ¶
The package provides four main operations:
- MakeIORef: Creates a new IORef with an initial value
- Read: Atomically reads the current value from an IORef
- Write: Atomically writes a new value to an IORef
- Modify: Atomically modifies the value using a transformation function
- ModifyWithResult: Atomically modifies the value and returns a computed result
Thread Safety ¶
All operations on IORef are thread-safe:
- Read operations use read locks, allowing multiple concurrent readers
- Write and Modify operations use write locks, ensuring exclusive access
- The underlying sync.RWMutex ensures proper synchronization
Basic Usage ¶
Creating and using an IORef:
import (
"github.com/IBM/fp-go/v2/ioref"
)
// Create a new IORef
ref := ioref.MakeIORef(42)()
// Read the current value
value := ioref.Read(ref)() // 42
// Write a new value
ioref.Write(100)(ref)()
// Read the updated value
newValue := ioref.Read(ref)() // 100
Modifying Values ¶
Use Modify to transform the value in place:
ref := ioref.MakeIORef(10)()
// Double the value
ioref.Modify(func(x int) int { return x * 2 })(ref)()
// Chain multiple modifications
ioref.Modify(func(x int) int { return x + 5 })(ref)()
ioref.Modify(func(x int) int { return x * 3 })(ref)()
result := ioref.Read(ref)() // (10 * 2 + 5) * 3 = 75
Atomic Modify with Result ¶
Use ModifyWithResult when you need to both transform the value and compute a result from the old value in a single atomic operation:
ref := ioref.MakeIORef(42)()
// Increment and return the old value
oldValue := ioref.ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+1, x)
})(ref)()
// oldValue is 42, ref now contains 43
This is particularly useful for implementing counters, swapping values, or any operation where you need to know the previous state.
Concurrent Usage ¶
IORef is safe to use across multiple goroutines:
ref := ioref.MakeIORef(0)()
// Multiple goroutines can safely modify the same IORef
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
ioref.Modify(func(x int) int { return x + 1 })(ref)()
}()
}
wg.Wait()
result := ioref.Read(ref)() // 100
Comparison with Haskell's IORef ¶
This implementation provides the following Haskell IORef operations:
- newIORef → MakeIORef
- readIORef → Read
- writeIORef → Write
- modifyIORef → Modify
- atomicModifyIORef → ModifyWithResult
The main difference is that Go's implementation uses explicit locking (sync.RWMutex) rather than relying on the runtime's STM (Software Transactional Memory) as Haskell does.
Performance Considerations ¶
IORef operations are highly optimized:
- Read operations are very fast (~5ns) and allow concurrent access
- Write and Modify operations are slightly slower (~7-8ns) due to exclusive locking
- ModifyWithResult is marginally slower (~9ns) due to tuple creation
- All operations have zero allocations in the common case
For high-contention scenarios, consider:
- Using multiple IORefs to reduce lock contention
- Batching modifications when possible
- Using Read locks for read-heavy workloads
Examples ¶
Counter with atomic increment:
counter := ioref.MakeIORef(0)()
increment := func() int {
return ioref.ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+1, x+1)
})(counter)()
}
id1 := increment() // 1
id2 := increment() // 2
id3 := increment() // 3
Shared configuration:
type Config struct {
MaxRetries int
Timeout time.Duration
}
configRef := ioref.MakeIORef(Config{
MaxRetries: 3,
Timeout: 5 * time.Second,
})()
// Update configuration
ioref.Modify(func(c Config) Config {
c.MaxRetries = 5
return c
})(configRef)()
// Read configuration
config := ioref.Read(configRef)()
Stack implementation:
type Stack []int
stackRef := ioref.MakeIORef(Stack{})()
push := func(value int) {
ioref.Modify(func(s Stack) Stack {
return append(s, value)
})(stackRef)()
}
pop := func() option.Option[int] {
return ioref.ModifyWithResult(func(s Stack) pair.Pair[Stack, option.Option[int]] {
if len(s) == 0 {
return pair.MakePair(s, option.None[int]())
}
return pair.MakePair(s[:len(s)-1], option.Some(s[len(s)-1]))
})(stackRef)()
}
Package ioref provides mutable references in the IO monad.
IORef represents a mutable reference that can be read and written within IO computations. It provides thread-safe access to shared mutable state using read-write locks.
This is inspired by Haskell's Data.IORef module and provides a functional approach to managing mutable state with explicit IO effects.
Example usage:
// Create a new IORef
ref := ioref.MakeIORef(42)()
// Read the current value
value := ioref.Read(ref)() // 42
// Write a new value
ioref.Write(100)(ref)()
// Modify the value
ioref.Modify(func(x int) int { return x * 2 })(ref)()
// Read the modified value
newValue := ioref.Read(ref)() // 200
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Modify ¶
Modify atomically modifies the value in an IORef using the given function.
This function returns a Kleisli arrow that takes an IORef and produces an IO computation that applies the transformation function to the current value. The modification is atomic and thread-safe, using a write lock to ensure exclusive access during the read-modify-write cycle.
Parameters:
- f: An endomorphism (function from A to A) that transforms the current value
Returns:
- A Kleisli arrow from IORef[A] to IO[IORef[A]]
Example:
ref := ioref.MakeIORef(42)()
// Double the value
ioref.Modify(func(x int) int { return x * 2 })(ref)()
// Chain multiple modifications
pipe.Pipe2(
ref,
ioref.Modify(func(x int) int { return x + 10 }),
io.Chain(ioref.Modify(func(x int) int { return x * 2 })),
)()
func ModifyWithResult ¶
ModifyWithResult atomically modifies the value in an IORef and returns both the new value and an additional result computed from the old value.
This function is useful when you need to both transform the stored value and compute some result based on the old value in a single atomic operation. It's similar to Haskell's atomicModifyIORef.
Parameters:
- f: A function that takes the old value and returns a Pair of (new value, result)
Returns:
- A Kleisli arrow from IORef[A] to IO[B] that produces the result
Example:
ref := ioref.MakeIORef(42)()
// Increment and return the old value
oldValue := ioref.ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+1, x)
})(ref)() // Returns 42, ref now contains 43
// Swap and return the old value
old := ioref.ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(100, x)
})(ref)() // Returns 43, ref now contains 100
Types ¶
type Endomorphism ¶
type Endomorphism[A any] = endomorphism.Endomorphism[A]
Endomorphism represents a function from A to A. It's commonly used with Modify to transform the value in an IORef.
type IO ¶
IO represents a synchronous computation that may have side effects. It's a function that takes no arguments and returns a value of type A.
func MakeIORef ¶
func MakeIORef[A any](a A) IO[IORef[A]]
MakeIORef creates a new IORef containing the given initial value.
This function returns an IO computation that, when executed, creates a new mutable reference initialized with the provided value. The reference is thread-safe and can be safely shared across goroutines.
Parameters:
- a: The initial value to store in the IORef
Returns:
- An IO computation that produces a new IORef[A]
Example:
// Create a new IORef with initial value 42
refIO := ioref.MakeIORef(42)
ref := refIO() // Execute the IO to get the IORef
// Create an IORef with a string
strRefIO := ioref.MakeIORef("hello")
strRef := strRefIO()
func Read ¶
func Read[A any](ref IORef[A]) IO[A]
Read atomically reads the current value from an IORef.
This function returns an IO computation that reads the value stored in the IORef. The read operation is thread-safe, using a read lock that allows multiple concurrent readers but excludes writers.
Parameters:
- ref: The IORef to read from
Returns:
- An IO computation that produces the current value of type A
Example:
ref := ioref.MakeIORef(42)()
// Read the current value
value := ioref.Read(ref)() // 42
// Use in a pipeline
result := pipe.Pipe2(
ref,
ioref.Read[int],
io.Map(func(x int) int { return x * 2 }),
)()
type IORef ¶
type IORef[A any] = *ioRef[A]
IORef represents a mutable reference to a value of type A. Operations on IORef are thread-safe and performed within the IO monad.
IORef provides a way to work with mutable state in a functional style, where mutations are explicit and contained within IO computations.