mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-11 00:42:26 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86a260a204 |
201
v2/ioref/doc.go
Normal file
201
v2/ioref/doc.go
Normal 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
208
v2/ioref/ioref.go
Normal 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
597
v2/ioref/ioref_test.go
Normal 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
74
v2/ioref/types.go
Normal 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]
|
||||
)
|
||||
@@ -370,5 +370,3 @@ func TestTailRec_ComplexState(t *testing.T) {
|
||||
assert.Equal(t, 60, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user