Context

Some values are needed almost everywhere: where output goes, what time it is, where random numbers come from, which database to use. Most languages make them globals, which are hard to replace in a test, or parameters, which every function in between has to pass along. Refingere has neither. They're context values: read by name anywhere, and provided by the code further up the call stack.

Reading a context value

&name reads the context value called name. The builtin ones cover everything a program touches outside itself - &stdout, &stderr and &stdin, the file system &fs, &env and &args, &clock and &random, &logger, &heapAllocator - plus a few policies such as &hashSeed. There's no other way to reach the terminal or the time, so a function's body - and its @require line - shows everything it touches outside itself. The Context reference lists them all.

Injecting a value

inject name := value provides a context value for the rest of the current block and for everything called from it, however deeply. When the block ends, so does the injection, and when several are in effect the nearest one wins. See Inject.

This follows calls, not the layout of the source: walk is written with no idea of the limit, and runs under whatever its caller injected. That's the difference from a parameter - a value reaches code many calls deep without every function in between passing it along - and from a global, which one part of the program would set for all of it.

Replacing the outside world in tests

Because code reaches the outside world only through the context, a test can replace any of it - capture output, fix the time, seed the randomness, give a function its own file system - and the code under test doesn't change. The common replacements are ready-made under #builtin.implementation: MemoryOutputStream, FixedClock, SeededRandom, MemoryFileSystem, MemoryLogger, TrackingAllocator and more - see Components.

Anything else is a type of your own. Each builtin value is typed by an interface - &clock by interface.Clock - and any type implementing it can be injected in its place.

An example: retrying with backoff

Code that sleeps and uses randomness is usually hard to test: the tests are slow, and flaky. This retry waits twice as long after each failure, plus random jitter - and its tests are instant and give the same result every time, without changing retry at all:

retry never calls the operating system. It waits with &clock.sleep(...) and draws its jitter from &random, so the test injects a FixedClock - whose sleep moves its time forward instead of waiting - and a SeededRandom, which gives the same numbers on every machine. The test can then check each wait exactly as it happened.

Declaring what a function needs

@require lists the context values a function needs - the ones it reads, and the ones the functions it calls need, unless it injects them itself. The compiler follows the calls and warns about a function that doesn't declare what it needs, so a function's attributes always say what it reaches outside itself, however deep the call that reaches it. @main and @test functions, which start with the builtins provided, declare nothing. See the rules for function values, defaults and effect attributes.

A context value is named with its & there, the way it's read. A name without one is a global const the function reads - @require(&stdout, MAX_USERS) - and a function that reads a global const has to name it, so its attributes show what it takes from outside itself. A const can't be injected, so only the function reading it names it, not its callers.

Your own context values

Any name can be a context value - a database, the current tenant, a feature flag. context name: Type declares one with its type, for its folder, its file with local, or the whole project with global. A value after := is the default, read where nothing up the call stack injected one. See Context Definition.

Injecting for a file, folder or project

A const marked @inject is injected into every call in its file, @injectLocally into every call in its folder, and @injectGlobally into every call in the project. This is where a program's shared dependencies live. An inject further down the call stack is nearer, so it still wins: a test injects its own database over the folder's. The Dependency Injection guide builds a whole application this way.

Ending an injection early

reject name removes the current block's own injection of a name, so reads see whatever was injected further out. Every block keeps its injections to itself, so an inject inside an if or a loop body ends with that block. See Reject.