Skip to content

App Fields, Resources & Context

An app field is a value on the runtime app value produced by fn boot: config data, dependency handlers, the per-life Context, or whatever else the program chooses to carry. Read one as MyApp.field; use with MyApp.field = value to rebind it for the rest of the current block. No globals, no service locators, no DI containers — just a typed app value and scoped overrides.

The smallest end-to-end example is plain config. fn boot produces the app value, and main reads a field from the active app type:

import {
  std/app: App
  std/context: Context
  std/io: IO
}

struct MyApp {
  field context: Context
  field port: Int
}

impl App for MyApp

fn boot(): App {
  MyApp{context: Context.root(), port: 8080}
}

fn main() {
  IO.print("listening on :${MyApp.port}")
}

The pieces:

  • MyApp — your project’s concrete app struct. Its impl for App entry is stdlib’s signal that this is the app type; the compiler infers the field set so MyApp.field reads and with MyApp.field = ... overrides are type-checked.
  • fn boot(): App — the optional app-entry companion to fn main. Define it when your program wants app fields beyond the default root Context. The runtime calls it once before fn main and stages the returned struct as the implicit app value for fn main and everything below it.
  • MyApp.port — a qualified read from the active app value. There is no bare port binding; the app type stays visible at the use site.

In a multi-file package, concrete app types may carry app-specific names such as MyApp, AdminApp, or WorkerApp.

A with MyApp.field = expr statement changes an active app field for the rest of the current block. Callees that read MyApp.field see the rebound value. Put the with near the top of a helper when a scoped override has a name in your program.

App fields can hold dependencies as well as config. Here logger is an interface value, and with swaps in a different implementation for one block:

import {
  std/app: App
  std/context: Context
  std/io: IO
}

interface Logger {
  fn log(logger: self, msg: String): Unit
}

type Stdout

impl Logger for Stdout {
  fn log(logger: Stdout, msg: String): Unit {
    IO.print("[log] ${msg}")
  }
}

struct PrefixedLogger {
  field tag: String
}

impl Logger for PrefixedLogger {
  fn log(logger: PrefixedLogger, msg: String): Unit {
    IO.print("[${logger.tag}] ${msg}")
  }
}

struct MyApp {
  field context: Context
  field logger: Logger
}

impl App for MyApp

fn boot(): App {
  MyApp{context: Context.root(), logger: Stdout}
}

fn main() {
  greet("World")
  audit_greet("Alice")
  greet("Bob")
}

fn audit_greet(name: String) {
  with MyApp.logger = PrefixedLogger{tag: "audit"}
  greet(name)
}

fn greet(name: String) {
  Logger.log(MyApp.logger, "hello, ${name}")
}

The with statement makes app-backed dependencies testable without test doubles or dependency injection scaffolding — just rebind. Multiple rebinds are just multiple sequential with statements.

Use resource name = expr for values that need a guaranteed close at the end of the current block. The value’s type implements Resource, and Nomi calls Resource.close automatically in reverse acquisition order. A function body is a block too; use an ordinary nested block when the resource should close before later statements in the same function continue:

import std/io: IO

struct Conn {
  field name: String
}

impl Resource for Conn {
  fn close(conn: Conn): Unit {
    IO.print("close ${conn.name}")
  }
}

fn main() {
  {
    resource first = Conn{name: "first"}
    resource second = Conn{name: "second"}
    IO.print("body")
  }

  IO.print("after")
}

Acquisition failure is ordinary control flow: write resource db = try Sqlite.temp() when setup returns Result. If the acquisition fails, db is not registered, and any earlier resources in the same block still close while the failure returns.

The runtime calls app-entry fn boot(): App once before fn main. The returned struct (any type with an impl App entry, typically your project’s concrete app struct) becomes the active app value. A qualified read such as MyApp.logger resolves against that value when MyApp is the active app type.

Scoped overrides propagate down the call chain. When a function calls another function, the callee sees any active with MyApp.field = ... values from the caller frame.

When there’s no user-defined fn boot, Nomi provides a default root context. Code that wants to read app fields explicitly should define an app type and boot.

with MyApp.field = expr rebinds an existing active app field for the remainder of the current block. You can’t pull a fresh app field out of thin air with with; the target must be a field on the active app type.

Multiple with statements are evaluated sequentially — a later rebind can reference an earlier one in scope:

with MyApp.logger = TaggedLogger{tag: "audit"}

with MyApp.clock = FakeClock{at: 12345}

audit_step()

After those statements, callees reading MyApp.logger see the new TaggedLogger; callees reading MyApp.clock see the new FakeClock. When the enclosing block exits, the rebindings revert.

Context — the per-life execution carrier

Section titled “Context — the per-life execution carrier”

The Context type in std/context is the cancellation/deadline carrier threaded through every Nomi program. A context field is still an ordinary app field: with MyApp.context = Context.with_timeout(MyApp.context, …) correctly threads the rebound context to every callee reached after the statement.

The stdlib surface (std/context):

pub extern type Context {
  // Construct the root while building the app value.
  pub extern fn root(): Context

  // Read state.
  pub extern fn canceled?(c: Context): Bool

  pub extern fn deadline(c: Context): Maybe<Instant>

  pub extern fn deadline_remaining(c: Context): Maybe<Duration>

  // Derive a child with a new deadline / timeout.
  pub extern fn with_deadline(c: Context, at: Instant): Context

  pub extern fn with_timeout(c: Context, dur: Duration): Context

  // Attach and retrieve typed contextual values.
  pub extern fn with_value<'T>(c: Context, value: 'T): Context

  pub extern fn value<'T>(c: Context, value_type: Type<'T>): Maybe<'T>
}

Context.with_deadline is the primitive; Context.with_timeout is sugar for Context.with_deadline(c, Instant.add(Instant.now(), dur)). Every derived Context inherits cancellation from its parent — derive a child for a sub-task and the parent’s cancellation cancels every descendant.

User code can’t construct Context from scratch — only Context.root(), the with_* derivers, and the runtime mint Context values.

Concurrent work inherits the active Context, so deadlines and cancellation flow through app fields too. The next chapter covers that shape when it introduces concurrent, Task.async, and Task.await.