From 35cd7774b80cf5faa11ff477edf1b5b69abe9580 Mon Sep 17 00:00:00 2001 From: Magnus Madsen Date: Tue, 10 Mar 2026 15:31:35 +0100 Subject: [PATCH] feat: add Clock, Sleep, Random, Logger, Env, and Exit library effect pages Add six new documentation pages for library effects and update the index table to group effects by module with code-font links. Also add full effect/module signature sections to Process and Logger. Co-Authored-By: Claude Opus 4.6 --- src/SUMMARY.md | 6 +++ src/clock.md | 61 ++++++++++++++++++++++ src/env.md | 110 +++++++++++++++++++++++++++++++++++++++ src/exit.md | 34 +++++++++++++ src/library-effects.md | 18 ++++--- src/logger.md | 72 ++++++++++++++++++++++++++ src/process.md | 74 +++++++++++++++++++++++---- src/random.md | 57 +++++++++++++++++++++ src/sleep.md | 113 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 529 insertions(+), 16 deletions(-) create mode 100644 src/clock.md create mode 100644 src/env.md create mode 100644 src/exit.md create mode 100644 src/logger.md create mode 100644 src/random.md create mode 100644 src/sleep.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 184969f4..0eee67b5 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -35,9 +35,15 @@ - [Effect-Oriented Programming](./effect-oriented-programming.md) - [Library Effects](./library-effects.md) - [Assert](./assert.md) + - [Clock](./clock.md) - [Console](./console.md) + - [Env](./env.md) + - [Exit](./exit.md) - [Http and Https](./http-and-https.md) + - [Logger](./logger.md) - [Process](./process.md) + - [Random](./random.md) + - [Sleep](./sleep.md) - [Modules](./modules.md) - [Declaring Modules](./declaring-modules.md) - [Using Modules](./using-modules.md) diff --git a/src/clock.md b/src/clock.md new file mode 100644 index 00000000..a98d4fbc --- /dev/null +++ b/src/clock.md @@ -0,0 +1,61 @@ +# Clock + +Flix provides `Clock` as a library effect for querying the current wall-clock +time. The `Clock` effect has a default handler, so no explicit `runWithIO` call +is needed in `main`. The key module is `Time.Clock`. + +## The Clock Effect + +The `Clock` effect has a single operation that returns the time since the epoch +in a given unit: + +```flix +pub eff Clock { + /// Returns a measure of time since the epoch in the given time unit `u`. + def currentTime(u: TimeUnit): Int64 +} +``` + +The `TimeUnit` enum determines the granularity of the result: + +```flix +pub enum TimeUnit with Eq, ToString { + case Days, + case Hours, + case Microseconds, + case Milliseconds, + case Minutes, + case Nanoseconds, + case Seconds +} +``` + +## Reading the Current Time + +The simplest use of `Clock` is to read the current time and print it: + +```flix +use Time.Clock +use Time.TimeUnit + +def main(): Unit \ { Clock, IO } = + let timestamp = Clock.currentTime(TimeUnit.Milliseconds); + println("${timestamp} ms since the epoch") +``` + +Because `Clock` has a default handler, the effect is handled automatically. + +## The `now` Function + +The `Clock.now` function is a shorthand for +`Clock.currentTime(TimeUnit.Milliseconds)`: + +```flix +use Time.Clock + +def main(): Unit \ { Clock, IO } = + let before = Clock.now(); + // ... do some work ... + let after = Clock.now(); + println("Elapsed: ${after - before} ms") +``` diff --git a/src/env.md b/src/env.md new file mode 100644 index 00000000..bc8fa81a --- /dev/null +++ b/src/env.md @@ -0,0 +1,110 @@ +# Env + +Flix provides `Env` as a library effect for accessing environment variables, +system properties, and platform information. The `Env` effect has a default +handler, so no explicit `runWithIO` call is needed in `main`. The key module is +`Sys.Env`. + +## The Env Effect + +The `Env` effect provides operations for reading the program's environment: + +```flix +pub eff Env { + /// Returns the arguments passed to the program via the command line. + def getArgs(): List[String] + + /// Returns a map of the current system environment. + def getEnv(): Map[String, String] + + /// Returns the value of the specified environment variable. + def getVar(name: String): Option[String] + + /// Returns the system property by name. + def getProp(name: String): Option[String] + + /// Returns the operating system name. + def getOsName(): Option[String] + + /// Returns the operating system architecture. + def getOsArch(): Option[String] + + /// Returns the operating system version. + def getOsVersion(): Option[String] + + /// Returns the file separator. + def getFileSeparator(): String + + /// Returns the path separator. + def getPathSeparator(): String + + /// Returns the system line separator. + def getLineSeparator(): String + + /// Returns the user's current working directory. + def getCurrentWorkingDirectory(): Option[String] + + /// Returns the default temp file path. + def getTemporaryDirectory(): Option[String] + + /// Returns the user's account name. + def getUserName(): Option[String] + + /// Returns the user's home directory. + def getUserHomeDirectory(): Option[String] + + /// Returns the number of virtual processors available to the JVM. + def getVirtualProcessors(): Int32 +} +``` + +## Directories and OS Info + +The simplest use of `Env` is to query the current directory, home directory, +or operating system: + +```flix +use Sys.Env + +def main(): Unit \ { Env, IO } = + let home = Env.getUserHomeDirectory(); + println("Home: ${home}"); + let cwd = Env.getCurrentWorkingDirectory(); + println("CWD: ${cwd}"); + let os = Env.getOsName(); + println("OS: ${os}") +``` + +## Environment Variables + +Use `getVar` to read a single environment variable, or `getEnv` to retrieve all +of them as a map: + +```flix +use Sys.Env + +def main(): Unit \ { Env, IO } = + let path = Env.getVar("PATH"); + println("PATH: ${path}"); + let all = Env.getEnv(); + println("Total env vars: ${Map.size(all)}") +``` + +## System Information + +The `Env` effect also exposes platform details such as the OS architecture and +the number of available processors: + +```flix +use Sys.Env + +def main(): Unit \ { Env, IO } = + let name = Env.getOsName(); + let arch = Env.getOsArch(); + let version = Env.getOsVersion(); + let cpus = Env.getVirtualProcessors(); + println("OS: ${name}"); + println("Arch: ${arch}"); + println("Ver: ${version}"); + println("CPUs: ${cpus}") +``` diff --git a/src/exit.md b/src/exit.md new file mode 100644 index 00000000..b67994b3 --- /dev/null +++ b/src/exit.md @@ -0,0 +1,34 @@ +# Exit + +Flix provides `Exit` as a library effect for terminating the program. The `Exit` +effect has a default handler, so no explicit `runWithIO` call is needed in +`main`. The key module is `Sys.Exit`. + +## The Exit Effect + +The `Exit` effect has a single operation that immediately stops the JVM with a +given exit code: + +```flix +pub eff Exit { + /// Immediately exits the JVM with the specified `exitCode`. + def exit(exitCode: Int32): Void +} +``` + +The return type `Void` indicates that `exit` never returns normally. + +## Exiting the Program + +The simplest use of `Exit` is to terminate with a specific exit code: + +```flix +use Sys.Exit + +def main(): Unit \ { Exit, IO } = + println("Goodbye!"); + Exit.exit(0) +``` + +A zero exit code conventionally signals success, while a non-zero code signals +an error. diff --git a/src/library-effects.md b/src/library-effects.md index 371c8be3..7d0ef5c5 100644 --- a/src/library-effects.md +++ b/src/library-effects.md @@ -4,9 +4,15 @@ Flix provides several built-in library effects for common I/O operations. These effects all have default handlers, so no explicit `runWithIO` is needed in `main`. -| Effect | Module | Description | -|---------------------------------|---------------------------|--------------------------------------------------------------------------------------------------| -| [Assert](./assert.md) | `Assert` | Runtime assertions (`assertTrue`, `assertEq`, etc.) with configurable handlers. | -| [Console](./console.md) | `Sys.Console` | Terminal I/O: reading input, printing to stdout/stderr, prompts, and menus. | -| [Http / Https](./http-https.md) | `Net.Http`, `Net.Https` | Sending HTTP requests with a fluent API, middleware (retries, rate limiting, circuit breakers). | -| [Process](./process.md) | `Sys.Process` | Spawning and managing OS processes. | +| Effect | Description | +|------------------------------------------------------|------------------------------------------------------------------------------------------------| +| [`Assert`](./assert.md) | Runtime assertions (`assertTrue`, `assertEq`, etc.) with configurable handlers. | +| [`Logger`](./logger.md) | Structured logging at five severity levels with filtering and collection. | +| [`Math.Random`](./random.md) | Generating pseudorandom numbers, with optional seeded determinism. | +| [`Net.Http` / `Net.Https`](./http-and-https.md) | Sending HTTP requests with a fluent API, middleware (retries, rate limiting, circuit breakers). | +| [`Sys.Console`](./console.md) | Terminal I/O: reading input, printing to stdout/stderr, prompts, and menus. | +| [`Sys.Env`](./env.md) | Accessing environment variables, system properties, and platform information. | +| [`Sys.Exit`](./exit.md) | Terminating the program with a specific exit code. | +| [`Sys.Process`](./process.md) | Spawning and managing OS processes. | +| [`Time.Clock`](./clock.md) | Querying the current wall-clock time in various units. | +| [`Time.Sleep`](./sleep.md) | Pausing the current thread with composable middleware (jitter, caps, logging). | diff --git a/src/logger.md b/src/logger.md new file mode 100644 index 00000000..b99aa46e --- /dev/null +++ b/src/logger.md @@ -0,0 +1,72 @@ +# Logger + +Flix provides `Logger` as a library effect for structured logging. The `Logger` +effect has a default handler, so no explicit `runWithIO` call is needed in +`main`. The key module is `Logger`. + +## The Logger Effect + +The `Logger` effect has a single operation that logs a message at a given +severity: + +```flix +pub eff Logger { + /// Logs the given message `m` at the given severity `s`. + def log(s: Severity, m: RichString): Unit +} +``` + +The `Severity` enum defines five levels, from lowest to highest: + +```flix +pub enum Severity with Eq, Order, ToString { + case Trace + case Debug + case Info + case Warn + case Fatal +} +``` + +## The Logger Module + +The `Logger` module provides convenience functions: + +```flix +mod Logger { + /// Logs the message `m` at the Trace level. + def trace(m: a): Unit \ Logger with Formattable[a] + + /// Logs the message `m` at the Debug level. + def debug(m: a): Unit \ Logger with Formattable[a] + + /// Logs the message `m` at the Info level. + def info(m: a): Unit \ Logger with Formattable[a] + + /// Logs the message `m` at the Warn level. + def warn(m: a): Unit \ Logger with Formattable[a] + + /// Logs the message `m` at the Fatal level. + def fatal(m: a): Unit \ Logger with Formattable[a] +``` + +> **Note:** The logging functions accept any type that implements the +> `Formattable` trait. Most standard types (`String`, `Int32`, `Bool`, etc.) +> implement `Formattable`, so plain values can be logged directly. The trait +> converts values into `RichString`, which supports styled terminal output +> (colors, bold, etc.). + +## Logging Messages + +The convenience functions accept any value that implements `Formattable`: + +```flix +def main(): Unit \ { Logger } = + Logger.info("Application started"); + Logger.debug("Loading configuration..."); + Logger.warn("Cache size exceeds threshold"); + Logger.fatal("Unrecoverable error") +``` + +The default handler prints each message to standard output with a colored +severity prefix, for example: `[Info] Application started`. diff --git a/src/process.md b/src/process.md index ef4d5e6c..658ff4e3 100644 --- a/src/process.md +++ b/src/process.md @@ -4,6 +4,70 @@ Flix provides `Process` as a library effect for spawning and managing OS processes. The `Process` effect has a default handler, so no explicit `runWithIO` call is needed in `main`. The key module is `Sys.Process`. +## The Process Effect + +The `Process` effect supports spawning processes, accessing their I/O streams, +and waiting for completion: + +```flix +pub eff Process { + /// Executes `cmd` with `args`, working directory `cwd`, and environment `env`. + def execWithCwdAndEnv(cmd: String, args: List[String], + cwd: Option[String], env: Map[String, String]): + Result[IoError, ProcessHandle] + + /// Returns the exit value of the process `ph`. + def exitValue(ph: ProcessHandle): Result[IoError, Int32] + + /// Returns whether the process `ph` is alive. + def isAlive(ph: ProcessHandle): Result[IoError, Bool] + + /// Returns the PID of the process `ph`. + def pid(ph: ProcessHandle): Result[IoError, Int64] + + /// Returns the stdin stream of the process `ph`. + def stdin(ph: ProcessHandle): Result[IoError, StdIn] + + /// Returns the stdout stream of the process `ph`. + def stdout(ph: ProcessHandle): Result[IoError, StdOut] + + /// Returns the stderr stream of the process `ph`. + def stderr(ph: ProcessHandle): Result[IoError, StdErr] + + /// Stops the process `ph`. + def stop(ph: ProcessHandle): Result[IoError, Unit] + + /// Waits for the process `ph` to finish and returns its exit value. + def waitFor(ph: ProcessHandle): Result[IoError, Int32] + + /// Waits at most `time` (in the given `tUnit`) for the process `ph` to finish. + /// Returns `true` if the process exited, `false` if the timeout elapsed. + def waitForTimeout(ph: ProcessHandle, time: Int64, tUnit: TimeUnit): + Result[IoError, Bool] +} +``` + +## The Process Module + +The `S` module provides convenience functions built on the `Process` +effect: + +```flix +mod Process { + /// Executes the command `cmd` with the arguments `args`. + def exec(cmd: String, args: List[String]): + Result[IoError, ProcessHandle] \ Process + + /// Executes `cmd` with `args` and working directory `cwd`. + def execWithCwd(cmd: String, args: List[String], cwd: Option[String]): + Result[IoError, ProcessHandle] \ Process + + /// Executes `cmd` with `args` and environment variables `env`. + def execWithEnv(cmd: String, args: List[String], env: Map[String, String]): + Result[IoError, ProcessHandle] \ Process +} +``` + ## Executing a Command The simplest way to spawn an OS process is with `Process.exec`. It takes a @@ -86,13 +150,3 @@ def main(): Unit \ { Process, IO } = } ``` -## Other Operations - -The `Sys.Process` module provides several additional operations: - -- `Process.isAlive(ph)` — returns `true` if the process is still running -- `Process.pid(ph)` — returns the process ID -- `Process.stop(ph)` — terminates the process -- `Process.stdin(ph)` — returns the process's standard input stream (implements `Writable`) -- `Process.exitValue(ph)` — returns the exit code without blocking (fails if the process is still running) -- `Process.waitForTimeout(ph, time, tUnit)` — blocks until the process exits or the timeout expires diff --git a/src/random.md b/src/random.md new file mode 100644 index 00000000..b9b02db4 --- /dev/null +++ b/src/random.md @@ -0,0 +1,57 @@ +# Random + +Flix provides `Random` as a library effect for generating pseudorandom numbers. +The `Random` effect has a default handler, so no explicit `runWithIO` call is +needed in `main`. The key module is `Math.Random`. + +## The Random Effect + +The `Random` effect has two operations: + +```flix +pub eff Random { + /// Returns a pseudorandom 64-bit floating-point number in [0.0, 1.0]. + def randomFloat64(): Float64 + + /// Returns a pseudorandom 64-bit integer. + def randomInt64(): Int64 +} +``` + +## Generating Random Values + +The simplest use of `Random` is to generate a value and act on it: + +```flix +use Math.Random + +def main(): Unit \ { Random, IO } = + let flip = Random.randomFloat64() > 0.5; + if (flip) + println("heads") + else + println("tails") +``` + +Because `Random` has a default handler, the effect is handled automatically with +a fresh random seed. + +## Seeded Randomness + +The `runWithSeed` handler uses a fixed seed so that every run produces the same +sequence of random values. This is useful for reproducible tests and +benchmarks: + +```flix +use Math.Random + +def main(): Unit \ IO = + run { + let a = Random.randomFloat64(); + let b = Random.randomFloat64(); + println("a = ${a}, b = ${b}") + } with Random.runWithSeed(42i64) +``` + +Because a seeded random number generator is fully deterministic, `runWithSeed` +eliminates the `Random` effect without introducing `IO`. diff --git a/src/sleep.md b/src/sleep.md new file mode 100644 index 00000000..a82f4a2f --- /dev/null +++ b/src/sleep.md @@ -0,0 +1,113 @@ +# Sleep + +Flix provides `Sleep` as a library effect for pausing the current thread. The +`Sleep` effect has a default handler, so no explicit `runWithIO` call is needed +in `main`. The key module is `Time.Sleep`. + +## The Sleep Effect + +The `Sleep` effect has a single operation: + +```flix +pub eff Sleep { + /// Sleeps the current thread for the given duration `d`. + def sleep(d: Duration): Unit +} +``` + +Durations are created with helpers from the `Time.Duration` module, such as +`seconds`, `milliseconds`, `minutes`, and so on. + +## Basic Sleep + +The simplest use of `Sleep` is to pause for a fixed duration: + +```flix +use Time.Duration.seconds +use Time.Sleep + +def main(): Unit \ { Sleep, IO } = + println("Going to sleep..."); + Sleep.sleep(seconds(1)); + println("Woke up!") +``` + +## No-Op Sleep + +The `withNoOp` handler skips all sleeps, which is useful for testing code that +contains delays without actually waiting: + +```flix +use Time.Duration.seconds +use Time.Sleep + +def main(): Unit \ IO = + run { + println("Going to sleep..."); + Sleep.sleep(seconds(10)); + println("Woke up instantly!") + } with Sleep.withNoOp +``` + +Because `withNoOp` fully handles the `Sleep` effect, the result type no longer +includes `Sleep`. + +## Middleware + +The `Time.Sleep` module provides several middleware handlers that intercept and +transform sleep durations before forwarding to the underlying `Sleep` effect. +Middleware re-raises `Sleep`, so multiple layers can be composed. + +| Middleware | Description | +|---------------------|-----------------------------------------------------------| +| `withConstant` | Replaces every sleep duration with a fixed value. | +| `withScale` | Multiplies each duration by a factor. | +| `withMaxSleep` | Caps each individual sleep to a maximum. | +| `withMinSleep` | Ensures each sleep is at least a minimum. | +| `withMaxTotalSleep` | Caps the cumulative sleep across all calls to a budget. | +| `withJitter` | Adds random jitter (±factor) to each duration. | +| `withLogging` | Logs each sleep duration via the `Logger` effect. | +| `withCollect` | Collects all durations into a list instead of sleeping. | + +For example, `withMaxSleep` caps each sleep to a maximum and `withLogging` logs +each duration: + +```flix +use Time.Duration.{milliseconds, seconds} +use Time.Sleep + +def main(): Unit \ { Logger, Sleep, IO } = + run { + println("Sleeping for 2 seconds (capped to 500ms)..."); + Sleep.sleep(seconds(2)); + println("Done!") + } with Sleep.withMaxSleep(milliseconds(500)) + with Sleep.withLogging +``` + +## Composing Middleware + +Because each middleware re-raises `Sleep`, they stack naturally. The following +example adds ±20% random jitter to each sleep and logs the resulting durations: + +```flix +use Math.Random +use Time.Duration.{seconds} +use Time.Sleep + +/// Composes `withJitter` and `withLogging` to add ±20% random jitter +/// to sleep durations and log each sleep via the `Logger` effect. +def main(): Unit \ { Logger, Random, Sleep, IO } = + run { + println("Sleeping 3 times with ±20% jitter..."); + Sleep.sleep(seconds(1)); + Sleep.sleep(seconds(2)); + Sleep.sleep(seconds(3)); + println("Done!") + } with Sleep.withJitter(0.2) + with Sleep.withLogging +``` + +The order matters: `withJitter` intercepts the original durations, applies +jitter, and re-raises `Sleep`. The `withLogging` handler then sees the +jittered durations.