LEVIATHAN v962456e · 962456eee1

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; and bind T { ... } declare a factory binding for the type T. The body of the second form is a block that returns a T, 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-binding for how defaults and injection combine.
  • inject T written as an argument supplies the binding for T explicitly.
  • 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 bind written 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, IFileSystem and INet with system bindings. Writing use std::IConsole; brings the type in and activates its binding; see lang.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.