DocsLanguage

Resources and scopes.

Acquire resources around the work that uses them and release them on every language exit.

A file or socket has to be closed when the work using it ends, and that includes the case where the work fails. In Fango you don’t return a handle and hope someone closes it later. You put the work inside the resource’s callback scope, and the scope does the closing.

Keep file work inside its scope.

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

main() =
    result = Fail.attempt {
        File.withFile "input.txt" { file -> File.readLine file }
    }
    case result of
        Ok line -> print line
        Err error -> print (IO.describeError error)

File.withFile opens the file, calls your callback, and closes the file before it returns. The line you read is ordinary data, so it’s still usable afterwards. To run this example you need a local input.txt, because file access isn’t available in the browser playground.

Build a cleanup scope.

Runtime.Scope.bracket acquire release action splits resource work into its three parts. acquire is a Unit callback. Once it succeeds, action receives the resource, and release runs exactly once when the scope exits.

You can watch the ordering without opening a file. In this example, state records the steps of a small mock resource:

import Runtime.Scope as Scope
import State

main() =
    result = State.run "" {
        Scope.bracket
            { State.put "open"; "resource" }
            { _ -> State.put "#{State.get()} close" }
            { _ -> State.put "#{State.get()} use"; 42 }
    }
    print result.value
    print result.state
42
open use close

Release finishes before the normal result leaves the scope. If acquisition fails, nothing is released, because there’s nothing to release. If the body hits a language abort, release runs before the abort reaches its outer handler. Nested scopes release in the reverse of the order they were acquired.

Scope.finally action cleanup is the same pattern for when the cleanup needs no acquired value. Both are ordinary functions that take callbacks, so Fango needs no separate try or finally syntax.

Cleanup failures.

Cleanup can fail too. If the body succeeds but the cleanup fails, the cleanup failure becomes the result. If both fail, the body’s failure stays primary and the cleanup failure is recorded alongside it. Fail.attemptReport lets you inspect those secondary failures.

When you also need to know where the original failure happened, use Fail.attemptDiagnostic. It keeps the stack of the primary failure, and each suppressed cleanup failure can carry its own, read with Failure.trace.

Cleanup runs under the handlers that were in place where the release was defined. The guarantees cover normal returns and language aborts. Native panics aren’t automatically converted into language failures.

Resource lifetimes.

Returning a file handle doesn’t keep the file open. A later native operation will notice the closed resource and fail, and the same goes for a callback or stream that still refers to the handle. Consume such streams inside the scope that acquired the resource.

Scoped local permissions.

Buffered readers and writers follow a stronger, static rule: their fresh local permission can’t leave the callback. You can return parsed values or collected bytes, but returning the reader, its operation callbacks, or a stream that advances it is rejected.

Don’t confuse this with cleaning up an external resource. A byte reader’s scope doesn’t close the file feeding it, so nest the reader callback inside the file callback.

Resource types.

A library can mark an opaque type with {-# resource #-}, which identifies it as a resource regardless of how it’s represented. Its constructors and fields must stay private. The marker doesn’t acquire or release anything itself, so the library’s operations remain responsible for that.

Wrappers and staging.

Ordinary wrappers around bracket need no registration with the compiler. Stage-safe callbacks can run at compile time, but IO and external native resource operations can’t. Native integration explains where that boundary lies, for library authors.