Declarations
Dependency injection: bind and inject
Declare lexically scoped factory bindings that fill unbound parameters automatically, and select a binding explicitly with inject.
since 0.1.0-alpha.1linuxwindowswasm
Description
A bind declaration says how to build a value of a type wherever a call leaves a parameter of that
type unfilled. It lets a function ask for a collaborator as an ordinary parameter without every
caller having to pass it.
bind ILogger => ConsoleLogger(); // factory binding: an expression
bind ILogger { if (cfg) return A(); return B(); } // factory binding: a body
greet(); // injection is implicit when unambiguous
greet(inject ILogger); // explicit selector
A binding is a factory: it runs each time a value is needed, so every injection gets a fresh value.
The => expr form and the block form follow the same rules as a method body. A binding is
lexically scoped and the nearest binding of a type wins. Two bindings of the same type in one
scope is a compile error. When a call leaves a parameter unfilled and has no default for it, the
checker looks for a binding of the parameter's type, from the call site outward; inject Type as an
argument names the binding explicitly.
Factory bindings and nearest-wins
interface ILogger { string log(string m); }
class ConsoleLogger : ILogger { string log(string m) => "console: " + m; }
class FileLogger : ILogger { string log(string m) => "file: " + m; }
void greet(ILogger logger, string who = "world") {
console.writeln(logger.log("hello " + who));
}
bool useFile = true;
bind ILogger { if (useFile) { return FileLogger(); } return ConsoleLogger(); }
greet();
void inner() {
bind ILogger => ConsoleLogger();
greet();
greet(who: "inner");
}
inner();
greet();
greet(inject ILogger);
file: hello world
console: hello world
console: hello inner
file: hello world
file: hello world
The file-level binding supplies the logger in greet(). Inside inner, a nearer binding shadows it.
Rules
bind T => expr;andbind T { ... }declare a factory binding for the typeT. The body of the second form is a block that returns aT, with the same rules as a method body.- A binding is visible in the scope where it is declared, including nested scopes, and the nearest binding of a type wins.
- Declaring two bindings of the same type in the same scope is a compile error.
- A parameter is filled by injection only when it is not supplied by the call and has no default. See
lang.argument-bindingfor how defaults and injection combine. inject Twritten as an argument supplies the binding forTexplicitly.- Place the binding around the call, not around the callee. A binding is resolved at the site where the
unfilled parameter is bound, not along the call chain. A
bindwritten inside a function body only reaches the calls made from that body. A binding that should wire a whole program must be in a scope that encloses every call it should fill. - The standard library declares capability interfaces
IEnv,IConsole,IClock,IFileSystemandINetwith system bindings. Writinguse std::IConsole;brings the type in and activates its binding; seelang.bind-activation.
Examples
A binding must enclose the call it is meant to fill:
Bind placement
interface IEnv { string name(); }
class Real : IEnv { string name() => "real"; }
class Fake : IEnv { string name() => "fake"; }
void main(IEnv env) { console.writeln(env.name()); }
bind IEnv => Real();
void wrapper() {
bind IEnv => Fake();
main();
}
wrapper();
main();
void inner() {
bind IEnv => Fake();
}
inner();
main();
fake
real
real
The Fake binding in wrapper encloses the call to main() there, so that call is filled with
Fake. The binding inside inner encloses nothing that calls main, so the last call is unaffected.
Notes
Not implemented: binding a ready-made object (bind someObject;) and aggregating bindings into a
value (Bindings objects). A statement of the form bind expr; that is not bind Type => ... is
accepted but installs nothing. Use a factory binding instead, such as bind IConsole => recorder;, which
returns an existing object each time it runs.
See also
- Argument binding — How a call's arguments are matched to parameters, filled by defaults or injection, and how overloads are ranked.
- Activating a namespace's bind with use — Importing a type with use also installs the binding its namespace exports for that type.
- IConsole — What a program may say to the terminal.
- IEnv — What a program may learn from the process environment: its arguments and its variables.
- IClock — What a program may know about the time.
- IFileSystem — What a program may do with the file system: open files and check whether a path exists.
- INet — What a program may do with the network: open connections and listen for them.