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"
// 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))
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.
val doubled = io.Map[int, int](io.Of(21), (x int) => x * 2)
doubled.Run() // Success(42)
val chained = io.FlatMap[int, int](io.Of(10), (x int) => io.Of(x + 32))
chained.Run() // Success(42)
val program = io.AndThen[bool, string](
io.Effect(() => { Println("setup") }),
io.Of("done"),
)
program.Run() // prints "setup", returns Success("done")
io.ForEach(io.Of(42), (v) => { Println(v) }).Run()
// prints 42
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
val safe = io.Recover(
io.Fail[int](errors.New("oops")),
(err) => -1,
)
safe.Run() // Success(-1)
val retried = io.RecoverWith(
io.Fail[int](errors.New("oops")),
(err) => io.Suspend(() => fallbackComputation()),
)
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)
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) |
| 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 |