Nib is a small language with one idea: a function says what it needs and what it may do, the program is checked against that before it runs, and the one place a promise becomes an implementation is a line you can point at.
There is no framework, no dependency injection container, no annotation processor, and no mock library. There is an interface — what is needed, named. A module — the logic, allowed to name only the interface. An effector — the only thing that may hold state or touch the world. And a wire — the single seam where a promise meets an instance.
namespace Hello
-- 1. The promise. An interface says *what* is needed, never how it is done.
interface Greeting where
hello(name: String) -> String
-- 2. The world. An effector is the only thing in nib that may hold state or
-- touch anything outside the program, and here it holds none.
sequential effector English is Greeting where
hello(name) = "Hello, " ++ name ++ "!"
-- 3. The logic. Pure by construction: it declares the interface it needs and
-- cannot reach an instance even if it wanted to.
module Hello where
func main :: -> String ! Greeting =
@Greeting.hello("World")
-- 4. The wire. The one place a promise becomes an instance.
wire W where Hello.main with Greeting = Hello.English
$ nibc --run snippets/hello.nib Hello, World!
Notice what the program never does: it never constructs English,
never imports it, never checks whether it exists. main calls
@Greeting.hello and the wire decides who answers. That is the entire
architecture, and every other page here is a consequence of it.
var or reach the file system, the clock, or a socket. Marked
sequential when it has a lifecycle.with Io.Out = Io.Stdout. The only place an
instance is chosen. Change the wire, change the world — the module is
untouched, and the same file can be a test, a batch job, or a server.The receipt can't lie. Every function's signature lists the
effects its body performs, and the compiler refuses a body that does more. A
function that declares ! Store and quietly writes a log line does
not compile — you get the diagnostic, not the bug in production.
Swapping the world needs no test doubles. Because implementations arrive through a wire and never through the call site, a fixture is an ordinary effector: one word in a wire line and the module runs against fixed data. Nothing is monkey-patched.
Cross-cutting behaviour is composition, not annotation. An
effector can take another effector as a parameter and forward to it, so logging,
counting, or retrying is a second instance the wire pipes in — Plain |>
Counted — not a decorator the module has to know about.
Every guarantee on the next page ships with a program that proves it →
v0.1.0OCamlno packages
Nib is a working research language, not a 1.0. The type checker, effect rows,
loan checker, and HTTP server are real and this page is built with them: the
site you are reading is a nib program, served by nibc --serve, from
the same file that prints it. It is also single-threaded, has no TLS, and has no
routing table — a 404 here is a line of nib, not a framework default.
Read the limits before you trust it with anything real; that page is written by the same people and is not marketing.