Standard library · Language and runtime

wire

Dependency injection in the spirit of google/wire: named providers, interface bindings, cleanups and a graph check for missing dependencies and cycles.

import wire

wire wires the layers of a program together: a repository, a service that uses it, a handler that uses the service. You register one provider per component, name what each one needs, and ask for the top of the graph. Each component is built once, and Close runs the cleanups in the reverse of the order they were built.

Salam has no runtime type information, so providers are identified by a name you choose, usually the type name. A provider is a function that takes the container and returns the component; it reads its own dependencies with Get. The second string of Provide lists those dependencies so that Build can check the whole graph before anything runs.

Providers and the graph check

Provide(c, name, needs, witness, factory) registers a provider. needs is a comma separated list of provider names, empty when there are none. witness is any value of the component's type; it only tells the compiler which type the factory returns. Build returns an empty string when the graph is sound and otherwise a message that names the problem: a missing dependency, a duplicate provider or a dependency cycle with the full path.

wire-quickstart.salam
import wire

struct Repo:
    pub rows: int = 0
end

struct Service:
    pub repo: Repo = Repo {}
    pub func Count(): int:
        ret this.repo.rows
    end
end

struct Handler:
    pub svc: Service = Service {}
    pub func Serve(): str:
        ret "rows=" + this.svc.Count()
    end
end

func main:
    c := wire.New()
    wire.Provide(c, "Repo", "", Repo {}, (_c: wire.Container): ret Repo { rows = 3 } end)
    wire.Provide(c, "Service", "Repo", Service {}, (cx: wire.Container):
        mut r := Repo {}
        _ok := wire.Get(cx, "Repo", r)
        ret Service { repo = r }
    end)
    wire.Provide(c, "Handler", "Service", Handler {}, (cx: wire.Container):
        mut s := Service {}
        _ok := wire.Get(cx, "Service", s)
        ret Handler { svc = s }
    end)
    err := wire.Build(c)
    if err != "":
        println err
        ret 1
    end
    mut h := Handler {}
    _ok := wire.Get(c, "Handler", h)
    println h.Serve()
    wire.Close(c)
end
Outputrows=3

Get(c, name, out) builds the component if it does not exist yet and copies it into out. It returns false and sets Error when the name is unknown or a cycle is hit at run time.

Interfaces, values and cleanups

Bind(c, "Store", "MemStore") makes the name Store resolve to the provider MemStore, which is how a service asks for an interface and gets the implementation chosen at the wiring site. Value registers a ready made value such as a configuration struct. OnClose attaches a cleanup to a provider; Close runs the cleanups of the components that were actually built, last built first, and frees the container.

Built lists the names in the order they were built and Count the number of registered names.

Reading the errors

Build only looks at the registered names and their needs lists, it never runs a factory. A sound graph gives an empty string; the other three messages are missing dependency: Service needs Repo, duplicate provider: Repo and dependency cycle: A -> B -> C -> A. Call it once at startup and stop there if the text is not empty. Get with a name nobody registered returns false and leaves missing provider: Nope in Error.

wire-guide-errors.salam
import wire

func report(label: str, c: wire.Container):
    msg := wire.Build(c)
    println label + ":", msg == "" ? "sound" : msg
    wire.Close(c)
end

func main:
    ok := wire.New()
    wire.Provide(ok, "Repo", "", 0, (_c: wire.Container): ret 1 end)
    report("fine", ok)

    missing := wire.New()
    wire.Provide(missing, "Service", "Repo", 0, (_c: wire.Container): ret 1 end)
    report("missing", missing)

    twice := wire.New()
    wire.Provide(twice, "Repo", "", 0, (_c: wire.Container): ret 1 end)
    wire.Provide(twice, "Repo", "", 0, (_c: wire.Container): ret 2 end)
    report("duplicate", twice)

    loop := wire.New()
    wire.Provide(loop, "A", "B", 0, (_c: wire.Container): ret 1 end)
    wire.Provide(loop, "B", "C", 0, (_c: wire.Container): ret 2 end)
    wire.Provide(loop, "C", "A", 0, (_c: wire.Container): ret 3 end)
    report("cycle", loop)

    empty := wire.New()
    mut n := 0
    println "get:", wire.Get(empty, "Nope", n), wire.Error(empty)
    wire.Close(empty)
end
Outputfine: sound missing: missing dependency: Service needs Repo duplicate: duplicate provider: Repo cycle: dependency cycle: A -> B -> C -> A get: false missing provider: Nope

Choosing an implementation at the wiring site

Put the choice in one function that builds the container. Here Value stores the configuration, two providers make a Store in different ways, and Bind points the name Store at one of them depending on the environment. Code that asks for Store does not change. Only the components that something actually asked for are built: Built lists 1 name in the test container and 2 in the production one, out of 4 registered.

wire-guide-swap.salam
import wire

struct Config:
    pub env: str = ""
end

struct Store:
    pub kind: str = ""
end

func build(env: str): wire.Container:
    c := wire.New()
    wire.Value(c, "Config", Config { env = env })
    wire.Provide(c, "Mem", "", Store {}, (_c: wire.Container): ret Store { kind = "memory" } end)
    wire.Provide(c, "Disk", "Config", Store {}, (cx: wire.Container):
        mut cfg := Config {}
        _ok := wire.Get(cx, "Config", cfg)
        ret Store { kind = "disk for " + cfg.env }
    end)
    if env == "test":
        wire.Bind(c, "Store", "Mem")
    else:
        wire.Bind(c, "Store", "Disk")
    end
    ret c
end

func main:
    for_env := ["test", "prod"]
    each env in for_env:
        c := build(env)
        println env + ":", wire.Build(c) == "" ? "sound" : "broken"
        mut s := Store {}
        _ok := wire.Get(c, "Store", s)
        println "  store is", s.kind
        built := wire.Built(c)
        println "  built", built.len(), "of", wire.Count(c)
        built.free()
        wire.Close(c)
    end
end
Outputtest: sound store is memory built 1 of 4 prod: sound store is disk for prod built 2 of 4

Build once, close in reverse

Every provider is a singleton: asking twice for Cache runs its factory once and hands out copies of the same value. The cleanups registered with OnClose run from the last built component to the first, so the cache is closed before the database it holds. A second Close does nothing.

wire-guide-lifecycle.salam
import wire

mut made := 0

struct Db:
    pub id: int = 0
end

struct Cache:
    pub db: Db = Db {}
end

func main:
    c := wire.New()
    wire.Provide(c, "Db", "", Db {}, (_c: wire.Container):
        made += 1
        ret Db { id = 7 }
    end)
    wire.Provide(c, "Cache", "Db", Cache {}, (cx: wire.Container):
        mut d := Db {}
        _ok := wire.Get(cx, "Db", d)
        ret Cache { db = d }
    end)
    wire.OnClose(c, "Db", Db {}, (d: Db): println "closing db", d.id end)
    wire.OnClose(c, "Cache", Cache {}, (k: Cache): println "closing cache over", k.db.id end)

    before := wire.Built(c)
    println "before", before.len()
    before.free()
    mut a := Cache {}
    mut b := Cache {}
    _x := wire.Get(c, "Cache", a)
    _y := wire.Get(c, "Cache", b)
    println "factory runs", made
    built := wire.Built(c)
    println "order", built.get(0), built.get(1)
    built.free()
    wire.Close(c)
    wire.Close(c)
    println "done"
end
Outputbefore 0 factory runs 1 order Db Cache closing cache over 7 closing db 7 done

Types

Container

struct Container
A dependency container: the registered providers and the components built so far. Copies of it share the same state.
NameTypeDescription
stState*The shared state of the container.

Functions

New

func New(): Container
Creates an empty container.
wire-Provide.salam
import wire

func main:
    c := wire.New()
    wire.Provide(c, "Base", "", 0, (_c: wire.Container): ret 10 end)
    wire.Provide(c, "Double", "Base", 0, (cx: wire.Container):
        mut b := 0
        _ok := wire.Get(cx, "Base", b)
        ret b * 2
    end)
    mut d := 0
    if wire.Get(c, "Double", d):
        println d
    end
    wire.Close(c)
end
Output20

Provide

func Provide<T>(c: Container, name: str, needs: str, _witness: T, factory: func(Container): T)
Registers a provider under a name. needs is a comma separated list of provider names, the witness is any value of the component type and only fixes T, and factory builds the component.

The example for this item is shown with the ones above: wire-Provide.salam

Value

func Value<T>(c: Container, name: str, v: T)
Registers a ready value, such as a configuration, under a name.
wire-Value.salam
import wire

struct Config:
    pub port: int = 0
end

func main:
    c := wire.New()
    wire.Value(c, "Config", Config { port = 8080 })
    mut cfg := Config {}
    if wire.Get(c, "Config", cfg):
        println cfg.port
    end
    wire.Close(c)
end
Output8080

Bind

func Bind(c: Container, iface: str, concrete: str)
Makes the name iface resolve to the provider concrete.
wire-Bind.salam
import wire

interface Greeter:
    func Hello(): str
end

struct English:
    pub func Hello(): str:
        ret "hello"
    end
end

func main:
    c := wire.New()
    wire.Provide(c, "English", "", English {}, (_c: wire.Container): ret English {} end)
    wire.Bind(c, "Greeter", "English")
    mut g := English {}
    if wire.Get(c, "Greeter", g):
        println g.Hello()
    end
    wire.Close(c)
end
Outputhello

OnClose

func OnClose<T>(c: Container, name: str, _witness: T, f: func(T))
Attaches a cleanup to a provider. Close calls it if the component was built.
wire-OnClose.salam
import wire

struct Conn:
    pub id: int = 0
end

func main:
    c := wire.New()
    wire.Provide(c, "Conn", "", Conn {}, (_c: wire.Container): ret Conn { id = 7 } end)
    wire.OnClose(c, "Conn", Conn {}, (v: Conn): println "closing", v.id end)
    mut x := Conn {}
    _ok := wire.Get(c, "Conn", x)
    wire.Close(c)
end
Outputclosing 7

Build

func Build(c: Container): str
Checks the graph without building anything. Returns an empty text when it is sound, otherwise a message that names the missing dependency, the duplicate provider or the cycle.
wire-Build.salam
import wire

func main:
    c := wire.New()
    wire.Provide(c, "Service", "Repo", 0, (_c: wire.Container): ret 1 end)
    println wire.Build(c)
    wire.Provide(c, "Repo", "Service", 0, (_c: wire.Container): ret 2 end)
    println wire.Build(c)
    wire.Close(c)
end
Outputmissing dependency: Service needs Repo dependency cycle: Service -> Repo -> Service

Get

func Get<T>(c: Container, name: str, out &: T): bool
Builds the component if it does not exist yet and copies it into out. Returns false and sets Error when the name is unknown or a cycle is reached.

The example for this item is shown with the ones above: wire-Provide.salam

Error

func Error(c: Container): str
The last error of the container, empty when there is none.

Count

func Count(c: Container): int
The number of registered names.

Built

func Built(c: Container): Vector<str>
The names that have been built, in the order they were built.

Close

func Close(c: Container)
Runs the cleanups starting from the last component built, and frees the container. Safe to call twice.

The example for this item is shown with the ones above: wire-OnClose.salam