1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-01-11 00:42:26 +02:00

Compare commits

...

1 Commits
v2.1.0 ... main

Author SHA1 Message Date
Carsten Leue
86a260a204 Introduce IORef (#150)
* fix: add ioref and tests

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: better tests

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

---------

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-04 16:45:40 +01:00
6 changed files with 1080 additions and 4 deletions

201
v2/ioref/doc.go Normal file
View File

@@ -0,0 +1,201 @@
// Copyright (c) 2023 - 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 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

208
v2/ioref/ioref.go Normal file
View File

@@ -0,0 +1,208 @@
// Copyright (c) 2023 - 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 ioref
import (
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
)
// 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()
//
//go:inline
func MakeIORef[A any](a A) IO[IORef[A]] {
return func() IORef[A] {
return &ioRef[A]{a: a}
}
}
// Write atomically writes a new value to an IORef.
//
// This function returns a Kleisli arrow that takes an IORef and produces an IO
// computation that writes the new value. The write operation is atomic and
// thread-safe, using a write lock to ensure exclusive access.
//
// The function returns the IORef itself, allowing for easy chaining of operations.
//
// Parameters:
// - a: The new value to write to the IORef
//
// Returns:
// - A Kleisli arrow from IORef[A] to IO[IORef[A]]
//
// Example:
//
// ref := ioref.MakeIORef(42)()
//
// // Write a new value
// ioref.Write(100)(ref)()
//
// // Chain multiple writes
// pipe.Pipe2(
// ref,
// ioref.Write(200),
// io.Chain(ioref.Write(300)),
// )()
//
//go:inline
func Write[A any](a A) io.Kleisli[IORef[A], IORef[A]] {
return func(ref IORef[A]) IO[IORef[A]] {
return func() IORef[A] {
ref.mu.Lock()
defer ref.mu.Unlock()
ref.a = a
return ref
}
}
}
// 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 }),
// )()
//
//go:inline
func Read[A any](ref IORef[A]) IO[A] {
return func() A {
ref.mu.RLock()
defer ref.mu.RUnlock()
return ref.a
}
}
// 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 })),
// )()
//
//go:inline
func Modify[A any](f Endomorphism[A]) io.Kleisli[IORef[A], IORef[A]] {
return func(ref IORef[A]) IO[IORef[A]] {
return func() IORef[A] {
ref.mu.Lock()
defer ref.mu.Unlock()
ref.a = f(ref.a)
return ref
}
}
}
// 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
//
//go:inline
func ModifyWithResult[A, B any](f func(A) Pair[A, B]) io.Kleisli[IORef[A], B] {
return func(ref IORef[A]) IO[B] {
return func() B {
ref.mu.Lock()
defer ref.mu.Unlock()
result := f(ref.a)
ref.a = pair.Head(result)
return pair.Tail(result)
}
}
}

597
v2/ioref/ioref_test.go Normal file
View File

@@ -0,0 +1,597 @@
// Copyright (c) 2023 - 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 ioref
import (
"sync"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
func TestMakeIORef(t *testing.T) {
t.Run("creates IORef with initial value", func(t *testing.T) {
value := F.Pipe2(
42,
MakeIORef[int],
io.Chain(Read[int]),
)()
assert.Equal(t, 42, value)
})
t.Run("creates IORef with string value", func(t *testing.T) {
value := F.Pipe2(
"hello",
MakeIORef[string],
io.Chain(Read[string]),
)()
assert.Equal(t, "hello", value)
})
t.Run("creates IORef with struct value", func(t *testing.T) {
type Person struct {
Name string
Age int
}
person := Person{Name: "Alice", Age: 30}
value := F.Pipe2(
person,
MakeIORef[Person],
io.Chain(Read[Person]),
)()
assert.Equal(t, person, value)
})
t.Run("creates IORef with slice value", func(t *testing.T) {
slice := []int{1, 2, 3, 4, 5}
value := F.Pipe2(
slice,
MakeIORef[[]int],
io.Chain(Read[[]int]),
)()
assert.Equal(t, slice, value)
})
t.Run("creates IORef with zero value", func(t *testing.T) {
value := F.Pipe2(
0,
MakeIORef[int],
io.Chain(Read[int]),
)()
assert.Equal(t, 0, value)
})
}
func TestRead(t *testing.T) {
t.Run("reads the current value", func(t *testing.T) {
value := F.Pipe2(
100,
MakeIORef[int],
io.Chain(Read[int]),
)()
assert.Equal(t, 100, value)
})
t.Run("reads value multiple times", func(t *testing.T) {
ref := MakeIORef(42)()
value1 := Read(ref)()
value2 := Read(ref)()
value3 := Read(ref)()
assert.Equal(t, 42, value1)
assert.Equal(t, 42, value2)
assert.Equal(t, 42, value3)
})
t.Run("reads updated value", func(t *testing.T) {
value := F.Pipe3(
10,
MakeIORef[int],
io.Chain(Write(20)),
io.Chain(Read[int]),
)()
assert.Equal(t, 20, value)
})
}
func TestWrite(t *testing.T) {
t.Run("writes a new value", func(t *testing.T) {
value := F.Pipe3(
42,
MakeIORef[int],
io.Chain(Write(100)),
io.Chain(Read[int]),
)()
assert.Equal(t, 100, value)
})
t.Run("overwrites previous value", func(t *testing.T) {
value := F.Pipe5(
1,
MakeIORef[int],
io.Chain(Write(2)),
io.Chain(Write(3)),
io.Chain(Write(4)),
io.Chain(Read[int]),
)()
assert.Equal(t, 4, value)
})
t.Run("returns the IORef for chaining", func(t *testing.T) {
ref := F.Pipe2(
10,
MakeIORef[int],
io.Chain(Write(20)),
)()
assert.NotNil(t, ref)
assert.Equal(t, 20, Read(ref)())
})
t.Run("writes different types", func(t *testing.T) {
strValue := F.Pipe3(
"initial",
MakeIORef[string],
io.Chain(Write("updated")),
io.Chain(Read[string]),
)()
assert.Equal(t, "updated", strValue)
boolValue := F.Pipe3(
false,
MakeIORef[bool],
io.Chain(Write(true)),
io.Chain(Read[bool]),
)()
assert.Equal(t, true, boolValue)
})
}
func TestModify(t *testing.T) {
t.Run("modifies value with function", func(t *testing.T) {
value := F.Pipe3(
10,
MakeIORef[int],
io.Chain(Modify(N.Mul(2))),
io.Chain(Read[int]),
)()
assert.Equal(t, 20, value)
})
t.Run("chains multiple modifications", func(t *testing.T) {
value := F.Pipe5(
5,
MakeIORef[int],
io.Chain(Modify(N.Add(10))),
io.Chain(Modify(N.Mul(2))),
io.Chain(Modify(N.Sub(5))),
io.Chain(Read[int]),
)()
assert.Equal(t, 25, value) // (5 + 10) * 2 - 5 = 25
})
t.Run("modifies string value", func(t *testing.T) {
value := F.Pipe3(
"hello",
MakeIORef[string],
io.Chain(Modify(func(s string) string { return s + " world" })),
io.Chain(Read[string]),
)()
assert.Equal(t, "hello world", value)
})
t.Run("returns the IORef for chaining", func(t *testing.T) {
ref := F.Pipe2(
42,
MakeIORef[int],
io.Chain(Modify(N.Add(1))),
)()
assert.NotNil(t, ref)
assert.Equal(t, 43, Read(ref)())
})
t.Run("modifies slice by appending", func(t *testing.T) {
value := F.Pipe3(
[]int{1, 2, 3},
MakeIORef[[]int],
io.Chain(Modify(func(s []int) []int { return append(s, 4, 5) })),
io.Chain(Read[[]int]),
)()
assert.Equal(t, []int{1, 2, 3, 4, 5}, value)
})
}
func TestModifyWithResult(t *testing.T) {
t.Run("modifies and returns result", func(t *testing.T) {
oldValue := F.Pipe2(
42,
MakeIORef[int],
io.Chain(ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+1, x)
})),
)()
assert.Equal(t, 42, oldValue)
})
t.Run("swaps value and returns old", func(t *testing.T) {
old := F.Pipe2(
100,
MakeIORef[int],
io.Chain(ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(200, x)
})),
)()
assert.Equal(t, 100, old)
})
t.Run("computes result from old value", func(t *testing.T) {
doubled := F.Pipe2(
10,
MakeIORef[int],
io.Chain(ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+5, x*2)
})),
)()
assert.Equal(t, 20, doubled) // old value * 2
})
t.Run("returns different type", func(t *testing.T) {
message := F.Pipe2(
42,
MakeIORef[int],
io.Chain(ModifyWithResult(func(x int) pair.Pair[int, string] {
return pair.MakePair(x*2, "doubled")
})),
)()
assert.Equal(t, "doubled", message)
})
t.Run("chains multiple ModifyWithResult calls", func(t *testing.T) {
ref := MakeIORef(5)()
result1 := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+10, x)
})(ref)()
assert.Equal(t, 5, result1)
result2 := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x*2, x)
})(ref)()
assert.Equal(t, 15, result2)
finalValue := Read(ref)()
assert.Equal(t, 30, finalValue)
})
}
func TestConcurrency(t *testing.T) {
t.Run("concurrent reads are safe", func(t *testing.T) {
ref := MakeIORef(42)()
var wg sync.WaitGroup
for range 100 {
wg.Add(1)
go func() {
defer wg.Done()
value := Read(ref)()
assert.Equal(t, 42, value)
}()
}
wg.Wait()
})
t.Run("concurrent writes are safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
for i := range 100 {
wg.Add(1)
go func(val int) {
defer wg.Done()
Write(val)(ref)()
}(i)
}
wg.Wait()
// Final value should be one of the written values
value := Read(ref)()
assert.GreaterOrEqual(t, value, 0)
assert.Less(t, value, 100)
})
t.Run("concurrent modifications are safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
for range 100 {
wg.Add(1)
go func() {
defer wg.Done()
Modify(N.Add(1))(ref)()
}()
}
wg.Wait()
value := Read(ref)()
assert.Equal(t, 100, value)
})
t.Run("concurrent ModifyWithResult is safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
results := make([]int, 100)
for i := range 100 {
wg.Add(1)
go func(idx int) {
defer wg.Done()
old := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+1, x)
})(ref)()
results[idx] = old
}(i)
}
wg.Wait()
// Final value should be 100
assert.Equal(t, 100, Read(ref)())
// All returned old values should be unique and in range [0, 99]
seen := make(map[int]bool)
for _, v := range results {
assert.GreaterOrEqual(t, v, 0)
assert.Less(t, v, 100)
assert.False(t, seen[v], "duplicate old value: %d", v)
seen[v] = true
}
})
t.Run("mixed concurrent operations are safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
// Concurrent reads
for range 50 {
wg.Add(1)
go func() {
defer wg.Done()
Read(ref)()
}()
}
// Concurrent writes
for i := range 25 {
wg.Add(1)
go func(val int) {
defer wg.Done()
Write(val)(ref)()
}(i)
}
// Concurrent modifications
for range 25 {
wg.Add(1)
go func() {
defer wg.Done()
Modify(N.Add(1))(ref)()
}()
}
wg.Wait()
// Should complete without deadlock or race conditions
value := Read(ref)()
assert.NotNil(t, value)
})
}
func TestEdgeCases(t *testing.T) {
t.Run("IORef with nil pointer", func(t *testing.T) {
var ptr *int
value := F.Pipe2(
ptr,
MakeIORef[*int],
io.Chain(Read[*int]),
)()
assert.Nil(t, value)
newPtr := new(int)
*newPtr = 42
updatedValue := F.Pipe3(
ptr,
MakeIORef[*int],
io.Chain(Write(newPtr)),
io.Chain(Read[*int]),
)()
assert.NotNil(t, updatedValue)
assert.Equal(t, 42, *updatedValue)
})
t.Run("IORef with empty slice", func(t *testing.T) {
value := F.Pipe2(
[]int{},
MakeIORef[[]int],
io.Chain(Read[[]int]),
)()
assert.Empty(t, value)
updatedValue := F.Pipe3(
[]int{},
MakeIORef[[]int],
io.Chain(Modify(func(s []int) []int { return append(s, 1) })),
io.Chain(Read[[]int]),
)()
assert.Equal(t, []int{1}, updatedValue)
})
t.Run("IORef with empty string", func(t *testing.T) {
value := F.Pipe2(
"",
MakeIORef[string],
io.Chain(Read[string]),
)()
assert.Equal(t, "", value)
updatedValue := F.Pipe3(
"",
MakeIORef[string],
io.Chain(Write("not empty")),
io.Chain(Read[string]),
)()
assert.Equal(t, "not empty", updatedValue)
})
t.Run("identity modification", func(t *testing.T) {
value := F.Pipe3(
42,
MakeIORef[int],
io.Chain(Modify(F.Identity[int])),
io.Chain(Read[int]),
)()
assert.Equal(t, 42, value)
})
t.Run("ModifyWithResult with identity", func(t *testing.T) {
ref := MakeIORef(42)()
result := ModifyWithResult(pair.Of[int])(ref)()
assert.Equal(t, 42, result)
assert.Equal(t, 42, Read(ref)())
})
}
func TestComplexTypes(t *testing.T) {
t.Run("IORef with map", func(t *testing.T) {
m := map[string]int{"a": 1, "b": 2}
value := F.Pipe3(
m,
MakeIORef[map[string]int],
io.Chain(Modify(func(m map[string]int) map[string]int {
m["c"] = 3
return m
})),
io.Chain(Read[map[string]int]),
)()
assert.Equal(t, 3, len(value))
assert.Equal(t, 3, value["c"])
})
t.Run("IORef with channel", func(t *testing.T) {
ch := make(chan int, 1)
retrievedCh := F.Pipe2(
ch,
MakeIORef[chan int],
io.Chain(Read[chan int]),
)()
retrievedCh <- 42
value := <-retrievedCh
assert.Equal(t, 42, value)
})
t.Run("IORef with function", func(t *testing.T) {
fn := N.Mul(2)
retrievedFn := F.Pipe2(
fn,
MakeIORef[func(int) int],
io.Chain(Read[func(int) int]),
)()
result := retrievedFn(21)
assert.Equal(t, 42, result)
})
}
// Benchmark tests
func BenchmarkMakeIORef(b *testing.B) {
for b.Loop() {
MakeIORef(42)()
}
}
func BenchmarkRead(b *testing.B) {
ref := MakeIORef(42)()
for b.Loop() {
Read(ref)()
}
}
func BenchmarkWrite(b *testing.B) {
ref := MakeIORef(0)()
for i := 0; b.Loop(); i++ {
Write(i)(ref)()
}
}
func BenchmarkModify(b *testing.B) {
ref := MakeIORef(0)()
for b.Loop() {
Modify(N.Add(1))(ref)()
}
}
func BenchmarkModifyWithResult(b *testing.B) {
ref := MakeIORef(0)()
for b.Loop() {
ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+1, x)
})(ref)()
}
}
func BenchmarkConcurrentReads(b *testing.B) {
ref := MakeIORef(42)()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Read(ref)()
}
})
}
func BenchmarkConcurrentWrites(b *testing.B) {
ref := MakeIORef(0)()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Write(42)(ref)()
}
})
}
func BenchmarkConcurrentModify(b *testing.B) {
ref := MakeIORef(0)()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Modify(N.Add(1))(ref)()
}
})
}
//

74
v2/ioref/types.go Normal file
View File

@@ -0,0 +1,74 @@
// Copyright (c) 2023 - 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 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
package ioref
import (
"sync"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
)
type (
// ioRef is the internal implementation of a mutable reference.
// It uses a read-write mutex to ensure thread-safe access.
ioRef[A any] struct {
mu sync.RWMutex
a A
}
// 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.
IO[A any] = io.IO[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.
IORef[A any] = *ioRef[A]
// Endomorphism represents a function from A to A.
// It's commonly used with Modify to transform the value in an IORef.
Endomorphism[A any] = endomorphism.Endomorphism[A]
Pair[A, B any] = pair.Pair[A, B]
)

View File

@@ -370,5 +370,3 @@ func TestTailRec_ComplexState(t *testing.T) {
assert.Equal(t, 60, result)
})
}
// Made with Bob

View File

@@ -650,5 +650,3 @@ func TestRetrying_StackSafety(t *testing.T) {
assert.Equal(t, maxAttempts, finalResult)
assert.Equal(t, maxAttempts, attempts, "Should handle many retries without stack overflow")
}
// Made with Bob