DocsStandard library

Files, console, and sockets.

Use the console, files, process environment, and sockets as services you can swap for tests.

Programs reach the outside world through a few small effects called services: Console for the standard streams, File.FileSystem for files, Process.Environment for arguments and environment variables, and Net.Network for sockets. A program’s main runs each service it uses under that service’s system handler. Tests can run the same code under a handler that simulates it.

API reference: Console · File · Process · Net · IO

Console.

main() =
    print "Your name?"
    case readLine() of
        Nothing -> print "no input"
        Just line -> print "Hello, #{line.text}"

print writes display text followed by a newline, while write writes a string unchanged. readLine() returns an optional IO.Line, whose text leaves out the line terminator. These names all come from the prelude. For standard error, import Console and use Console.writeTo Console.Stderr text.

These functions perform Console and never raise an error. The handler owns stream failures: the system one ends the program with a diagnostic if the terminal goes away. Console.source and Console.sink adapt the streams for readers and writers.

Swap a service.

Console.runTest answers the console from bytes and captures what the program writes:

import Bytes
import Console

greet() =
    print "Your name?"
    case readLine() of
        Nothing -> print "no input"
        Just line -> print "Hello, #{line.text}"

main() =
    captured = Console.runTest (Bytes.fromString "Ana\n") greet
    print (Bytes.toString captured.stdout)
Just "Your name?\nHello, Ana\n"

greet is the same code a program would run against the terminal. Under runTest, its reads come from the input bytes, and the result keeps the action’s value along with stdout, stderr, and the input left unread, each exactly as written. Nothing reaches the real terminal, so main here prints only the captured text.

Every service works this way. Each handler removes only its own effect, so a simulated service makes code pure only when nothing else in its row reaches the host. This is what the Testing guide relies on to check programs that read and write.

File.

import File
import IO
import Result exposing (Result(..))

main() =
    case File.read "input.txt" of
        Ok text -> print text
        Err error -> print (IO.describeError error)

This complete program needs a local file, so it can’t run in the browser. File.read returns the whole text as a Result, replacing invalid UTF-8. writeAll, isDirectory, and size also return structured results.

An error has a kind, a path, and a message. To decide how to recover, match on a portable kind such as IO.NotFound or IO.PermissionDenied. To report an error to a person, use IO.describeError rather than parsing its wording.

For incremental or binary input, use File.withFile path action and read from its handle inside the callback with File.readLine or File.readBytes. withOutput creates or truncates a file, and withAppend appends, creating the file if needed. Each of them closes the file on a normal return or a language abort, and their operations raise Fail IO.Error, so wrap the scope in Fail.attempt to handle failures. File.source and File.sink adapt a handle for buffered readers and writers without owning it.

Line reads keep the line ending as "\n", "\r\n", or "", and replace malformed UTF-8. Byte reads keep arbitrary input exactly as it is. Relative paths refer to the running program’s working directory. For large input, combine a scoped handle with buffered readers instead of reading everything at once.

Files in memory.

File.runMemory image action runs file code against an in-memory filesystem instead of the disk:

import File

main() =
    run = File.runMemory File.empty {
        written = File.writeAll "greeting.txt" "Hello!"
        File.read "greeting.txt"
    }
    print run.value
    print (File.textAt "greeting.txt" run.image)
Ok "Hello!"
Ok "Hello!"

File.empty holds only the root directory. Build a starting image with File.withTextFile, withByteFile, and withDirectory, and read the final one with File.textAt. The runner returns the action’s value with the final image. Handles, line reads, and directory operations all work in memory. Each operation applies atomically, even when tasks share the runner. To test failure paths, File.runMemoryWith takes faults, such as a failed open or a write that stops after a number of bytes.

Process.

Import Process when your program takes command-line arguments or reads environment variables:

import Process

main() =
    case Process.args() of
        [] -> print "usage: greet NAME"
        [name | _] -> print "Hello, #{name}!"

Save it as greet.fango and run fango run greet.fango -- Ada to print Hello, Ada!. With no arguments it prints the usage message. The arguments are the ones passed after the source path, since compiler flags go before it.

Process.env "HOME" returns Just the variable’s value, or Nothing when it isn’t set, and Process.variables() returns all of them as a Dict. They perform Process.Environment. To run the same code with fixed values, use Process.runWith args variables action.

A program reports its exit status by returning a Process.Status from main, as described in Entry points. Process.exit status ends the process immediately instead, without running cleanup or flushing buffered writers. In the interpreter it ends the current evaluation rather than shutting down the REPL that hosts it.

Net.

Net.withListener scopes a TCP listener, Net.accept scopes an accepted connection, and Net.withClient scopes an outgoing one. To use a connection, adapt it with Net.source and Net.sink, then add readers and writers as needed. Each call performs Net.Network, whose default handler uses the system’s sockets. A custom handler can answer the same operations without sockets, creating listeners and connections of its own through Net.withProvider. To test HTTP code, the HTTP client has its own simulated runners.

Net.Error.kind tells you whether a failure was a refusal, a reset, an address already in use, or a timeout. Other failures keep the system’s message.

The echo-server example puts these scopes together. Native sockets aren’t available in the browser playground.

Cancellation and shared handles.

Cancelling a task doesn’t automatically close a connection. The cancellation-aware acceptAsync, sourceAsync, and sinkAsync interrupt a blocked wait by closing the socket, and the task then ends as cancelled rather than with a network error. Read and write deadlines are a separate way to bound waits.

Sharing a handle between tasks doesn’t keep it open. Scope the owning resource around all of its users, and let Async drain the child tasks before the scope ends. A handle also belongs to the handler that opened it, and becomes invalid once that handler exits.

IO.

IO is the builtin effect underneath the services. It marks code that reaches the host directly, such as each service’s system handler, and no handler can intercept it. A few calls still require it because there’s nothing to simulate: Async.run starts real tasks, Time.Zone.load reads the host’s zone database, and Process.exit ends the process.