ioref

package
v2.2.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 5, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

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

func Modify[A any](f Endomorphism[A]) io.Kleisli[IORef[A], 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 })),
)()

func ModifyIOK added in v2.2.2

func ModifyIOK[A any](f io.Kleisli[A, A]) io.Kleisli[IORef[A], A]

ModifyIOK atomically modifies the value in an IORef using an IO-based transformation.

This is a more powerful version of Modify that allows the transformation function to perform IO effects. The function takes a Kleisli arrow (a function from A to IO[A]) and returns a Kleisli arrow that modifies the IORef atomically.

The modification is atomic and thread-safe, using a write lock to ensure exclusive access during the read-modify-write cycle. The IO effect in the transformation function is executed while holding the lock.

Parameters:

  • f: A Kleisli arrow (io.Kleisli[A, A]) that transforms the current value with IO effects

Returns:

  • A Kleisli arrow from IORef[A] to IO[A] that returns the new value

Example:

ref := ioref.MakeIORef(42)()

// Modify with an IO effect (e.g., logging)
modifyWithLog := ioref.ModifyIOK(func(x int) io.IO[int] {
    return func() int {
        fmt.Printf("Old value: %d\n", x)
        return x * 2
    }
})
newValue := modifyWithLog(ref)()  // Logs and returns 84

// Chain multiple IO-based modifications
pipe.Pipe2(
    ref,
    ioref.ModifyIOK(func(x int) io.IO[int] {
        return io.Of(x + 10)
    }),
    io.Chain(ioref.ModifyIOK(func(x int) io.IO[int] {
        return io.Of(x * 2)
    })),
)()

func ModifyIOKWithResult added in v2.2.2

func ModifyIOKWithResult[A, B any](f io.Kleisli[A, Pair[A, B]]) io.Kleisli[IORef[A], B]

ModifyIOKWithResult atomically modifies the value in an IORef and returns a result, using an IO-based transformation function.

This is a more powerful version of ModifyWithResult that allows the transformation function to perform IO effects. The function takes a Kleisli arrow that transforms the old value into an IO computation producing a Pair of (new value, result).

This is useful when you need to:

  • Both transform the stored value and compute some result based on the old value
  • Perform IO effects during the transformation (e.g., logging, validation)
  • Ensure atomicity of the entire read-transform-write-compute cycle

The modification is atomic and thread-safe, using a write lock to ensure exclusive access. The IO effect in the transformation function is executed while holding the lock.

Parameters:

  • f: A Kleisli arrow (io.Kleisli[A, Pair[A, B]]) that takes the old value and returns an IO computation producing 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 with IO effect and return old value
incrementWithLog := ioref.ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
    return func() pair.Pair[int, int] {
        fmt.Printf("Incrementing from %d\n", x)
        return pair.MakePair(x+1, x)
    }
})
oldValue := incrementWithLog(ref)()  // Logs and returns 42, ref now contains 43

// Swap with validation
swapWithValidation := ioref.ModifyIOKWithResult(func(old int) io.IO[pair.Pair[int, string]] {
    return func() pair.Pair[int, string] {
        if old < 0 {
            return pair.MakePair(0, "reset negative")
        }
        return pair.MakePair(100, fmt.Sprintf("swapped %d", old))
    }
})
message := swapWithValidation(ref)()

func ModifyReaderIOK added in v2.2.7

func ModifyReaderIOK[R, A any](f readerio.Kleisli[R, A, A]) readerio.Kleisli[R, IORef[A], A]

ModifyReaderIOK atomically modifies the value in an IORef using a ReaderIO-based transformation.

This is a variant of ModifyIOK that works with ReaderIO computations, allowing the transformation function to access an environment of type R while performing IO effects. This is useful when the modification logic needs access to configuration, context, or other shared resources.

The modification is atomic and thread-safe, using a write lock to ensure exclusive access during the read-modify-write cycle. The ReaderIO effect in the transformation function is executed while holding the lock.

Parameters:

  • f: A ReaderIO Kleisli arrow (readerio.Kleisli[R, A, A]) that takes the current value and an environment R, and returns an IO computation producing the new value

Returns:

  • A ReaderIO Kleisli arrow from IORef[A] to ReaderIO[R, A] that returns the new value

Example:

type Config struct {
    multiplier int
}

ref := ioref.MakeIORef(10)()

// Modify using environment
modifyWithConfig := ioref.ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
    return func(cfg Config) io.IO[int] {
        return func() int {
            return x * cfg.multiplier
        }
    }
})

config := Config{multiplier: 5}
newValue := modifyWithConfig(ref)(config)()  // Returns 50, ref now contains 50

func ModifyReaderIOKWithResult added in v2.2.7

func ModifyReaderIOKWithResult[R, A, B any](f readerio.Kleisli[R, A, Pair[A, B]]) readerio.Kleisli[R, IORef[A], B]

ModifyReaderIOKWithResult atomically modifies the value in an IORef and returns a result, using a ReaderIO-based transformation function.

This combines the capabilities of ModifyIOKWithResult and ModifyReaderIOK, allowing the transformation function to:

  • Access an environment of type R (like configuration or context)
  • Perform IO effects during the transformation
  • Both update the stored value and compute a result based on the old value
  • Ensure atomicity of the entire read-transform-write-compute cycle

The modification is atomic and thread-safe, using a write lock to ensure exclusive access. The ReaderIO effect in the transformation function is executed while holding the lock.

Parameters:

  • f: A ReaderIO Kleisli arrow (readerio.Kleisli[R, A, Pair[A, B]]) that takes the old value and an environment R, and returns an IO computation producing a Pair of (new value, result)

Returns:

  • A ReaderIO Kleisli arrow from IORef[A] to ReaderIO[R, B] that produces the result

Example:

type Config struct {
    logEnabled bool
}

ref := ioref.MakeIORef(42)()

// Increment with conditional logging, return old value
incrementWithLog := ioref.ModifyReaderIOKWithResult(
    func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
        return func(cfg Config) io.IO[pair.Pair[int, int]] {
            return func() pair.Pair[int, int] {
                if cfg.logEnabled {
                    fmt.Printf("Incrementing from %d\n", x)
                }
                return pair.MakePair(x+1, x)
            }
        }
    },
)

config := Config{logEnabled: true}
oldValue := incrementWithLog(ref)(config)()  // Logs and returns 42, ref now contains 43

func ModifyWithResult

func ModifyWithResult[A, B any](f func(A) Pair[A, B]) io.Kleisli[IORef[A], B]

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

func Write

func Write[A any](a A) io.Kleisli[IORef[A], A]

Write atomically writes a new value to an IORef and returns the written value.

This function returns a Kleisli arrow that takes an IORef and produces an IO computation that writes the given value to the reference. The write operation is atomic and thread-safe, using a write lock to ensure exclusive access.

Parameters:

  • a: The new value to write to the IORef

Returns:

  • A Kleisli arrow from IORef[A] to IO[A] that writes the value and returns it

Example:

ref := ioref.MakeIORef(42)()

// Write a new value
newValue := ioref.Write(100)(ref)()  // Returns 100, ref now contains 100

// Chain writes
pipe.Pipe2(
    ref,
    ioref.Write(50),
    io.Chain(ioref.Write(75)),
)()  // ref now contains 75

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.

An endomorphism is a morphism (structure-preserving map) from a mathematical object to itself. In programming terms, it's simply a function that takes a value and returns a value of the same type.

Example:

// An endomorphism that doubles an integer
double := func(x int) int { return x * 2 }

// An endomorphism that uppercases a string
upper := func(s string) string { return strings.ToUpper(s) }

// Use with IORef
ref := ioref.MakeIORef(21)()
ioref.Modify(double)(ref)()  // ref now contains 42

type IO

type IO[A any] = io.IO[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 computations are lazy - they don't execute until explicitly invoked by calling the function. This allows for composing and chaining effects before execution.

Example:

// Define an IO computation
computation := func() int {
    fmt.Println("Computing...")
    return 42
}

// Nothing happens yet - the computation is lazy
result := computation()  // Now it executes and prints "Computing..."

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. This makes side effects visible in the type system and allows for better reasoning about code that uses mutable state.

All operations on IORef (Read, Write, Modify, etc.) are atomic and thread-safe, making it safe to share IORefs across goroutines.

Example:

// 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 atomically
ioref.Modify(func(x int) int { return x * 2 })(ref)()

type Pair

type Pair[A, B any] = pair.Pair[A, B]

Pair represents a tuple of two values of types A and B. It's used with ModifyWithResult and ModifyIOKWithResult to return both a new value for the IORef (head) and a computed result (tail).

The head of the pair contains the new value to store in the IORef, while the tail contains the result to return from the operation. This allows atomic operations that both update the reference and compute a result based on the old value.

Example:

// Create a pair where head is the new value and tail is the old value
p := pair.MakePair(newValue, oldValue)

// Extract values
newVal := pair.Head(p)  // Gets the head (new value)
oldVal := pair.Tail(p)  // Gets the tail (old value)

// Use with ModifyWithResult to swap and return old value
ref := ioref.MakeIORef(42)()
oldValue := ioref.ModifyWithResult(func(x int) pair.Pair[int, int] {
    return pair.MakePair(100, x)  // Store 100, return old value
})(ref)()  // oldValue is 42, ref now contains 100

type ReaderIO added in v2.2.7

type ReaderIO[R, A any] = readerio.ReaderIO[R, A]

ReaderIO represents a computation that requires an environment of type R and produces an IO effect that yields a value of type A.

This combines the Reader pattern (dependency injection) with IO effects, allowing computations to access shared configuration or context while performing side effects.

Example:

type Config struct {
    multiplier int
}

// A ReaderIO that uses config to compute a value
computation := func(cfg Config) io.IO[int] {
    return func() int {
        return 42 * cfg.multiplier
    }
}

// Execute with specific config
result := computation(Config{multiplier: 2})()  // Returns 84

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL