IO[T] — Lazy Composable Effects

IO[T] is a lazy, composable effect type for separating pure and impure code. An IO[T] value describes a computation that produces T or fails — but does not execute it. Effects only run when you call .Run(). This makes side-effecting code referentially transparent: you can pass IO values around, compose them, and reason about them without triggering any effects.

import "martianoff/gala/io"

Creating IO Values

// Pure value — no side effects
val pure = io.Of(42)

// Lazy thunk — panics are caught as Failure
val suspended = io.Suspend(() => expensiveComputation())

// Failing IO
val failing = io.Fail[int](errors.New("boom"))

// Side-effecting void operation
val logging = io.Effect(() => { Println("log message") })

// From an existing Try
val fromTry = io.FromTry(Success(42))

Running IO

Nothing happens until you call .Run():

Println("Before IO creation")
val lazy = io.Suspend(() => {
    Println("Computing...")
    return 99
})
Println("IO created but not run")

// Now execute:
val result = lazy.Run()        // Try[int] — prints "Computing..."
val value = lazy.UnsafeRun()   // int — 99 (panics on failure)

Unlike Lazy[T], which caches its result, IO[T] re-executes on every .Run() call. This is intentional — side effects should be repeatable.


Composing Effects

Map — Transform the Result

val doubled = io.Map[int, int](io.Of(21), (x int) => x * 2)
doubled.Run()  // Success(42)

FlatMap — Chain Dependent Computations

val chained = io.FlatMap[int, int](io.Of(10), (x int) => io.Of(x + 32))
chained.Run()  // Success(42)

AndThen — Sequence, Discard First Result

val program = io.AndThen[bool, string](
    io.Effect(() => { Println("setup") }),
    io.Of("done"),
)
program.Run()  // prints "setup", returns Success("done")

ForEach — Side Effect on Success

io.ForEach(io.Of(42), (v) => { Println(v) }).Run()
// prints 42

Error Handling

Failures propagate through the chain automatically:

val failChain = io.FlatMap[int, int](
    io.Fail[int](errors.New("initial error")),
    (x int) => io.Of(x * 2),
)
failChain.Run().IsFailure()  // true — second step never runs

Recover — Handle Errors with a Value

val safe = io.Recover(
    io.Fail[int](errors.New("oops")),
    (err) => -1,
)
safe.Run()  // Success(-1)

RecoverWith — Handle Errors with a New IO

val retried = io.RecoverWith(
    io.Fail[int](errors.New("oops")),
    (err) => io.Suspend(() => fallbackComputation()),
)

Building Programs

IO shines when composing multi-step programs where each step may have side effects:

import "martianoff/gala/io"
import "errors"

func fetchData() int = 42
func process(n int) int = n * 2
func save(n int) bool {
    Println(s"Saving $n")
    return true
}

val program = io.FlatMap[int, bool](
    io.Suspend(fetchData),
    (data int) => io.FlatMap[int, bool](
        io.Of(process(data)),
        (result int) => io.Suspend(() => save(result)),
    ),
)

// Nothing has happened yet — program is just a description.
// Now run it:
program.Run()  // prints "Saving 84", returns Success(true)

IO vs Lazy

  Lazy[T] IO[T]
Re-execution Cached — .Get() returns same result Fresh — every .Run() re-executes
Thread safety Yes (via sync.Once) No memoization
Use case Expensive pure computations Side effects (HTTP, file I/O, logging)

API Reference

Function Description
io.Of(value) Pure value, no effects
io.Suspend(f) Lazy thunk, panics caught
io.Fail[T](err) Failing IO
io.Effect(f) Void side effect, returns IO[bool]
io.FromTry(t) Wrap existing Try
io.Unit() No-op IO, returns IO[bool]
io.Map[T, U](io, f) Transform success value
io.FlatMap[T, U](io, f) Chain dependent computations
io.AndThen[T, U](first, second) Sequence, discard first result
io.Recover(io, f) Handle error with fallback value
io.RecoverWith(io, f) Handle error with fallback IO
io.ForEach(io, f) Side effect on success
.Run() Execute, returns Try[T]
.UnsafeRun() Execute, returns T or panics

Further Reading