ADR-046: use Names a Unit of Code and Brings Nothing In

Status: Superseded by ADR-286 — its D1-D5 are D10-D14 there Related: ADR-047 D1 (which unit use names: a package, not a file — it moves this record’s boundary and leaves its rules standing), ADR-041 D1 (the “one form, not two spellings of it” rule this decision is taken on), ADR-030 D1 (use std::fs, the one use with a path in it), ADR-011 D2 (name-for-name lowering, which is why a qualified name reaches the emitter as written).

1. Context

use pool made a unit of code reachable, and nothing said whether it did anything else. The question is not decorative: it decides, for every name in every file, whether a reader can tell where that name comes from without leaving the line.

It became urgent for a smaller reason. A qualified type name did not resolve to the type its module’s own function handed back, so a module could offer behaviour and not data (#229’s cross-file type defect). Repairing that meant deciding what the qualified form is, because the repair is different if a name can also arrive unqualified.

2. Decisions

D1 — use makes a unit reachable, and does nothing else.

Every name from it is written with its prefix, at every use.

use pool
use request_handling as rh

fn handle(c: pool::Conn) -> rh::Response {        // in a type
    let d: pool::Conn = pool::make()              // in a type and in a call
    let e = pool::Conn(id: 1)                     // in a struct literal
    let s: Shared[pool::Conn] = pool::make()      // nested, like any other type
    return rh::ok(d.id + e.id)
}

The prefix falls where a name is written, not where a value is used. d.id and d.respond(200) carry none: there is a value in hand and nothing to resolve. In a body that works with values the prefix barely appears, which is what makes this bearable without an import form.

D2 — No name is brought in. Not by a glob, not by a list, not one at a time.

error: names are not brought in; a unit is reached through its name
   1 | use pool::{Conn, Pool}
       ^^^^^^^^^^^^^^^^^^^^^^
     = write `use pool`, and `pool::Conn` where you need it
     = if the prefix is long, `use pool as p` shortens it once, in one place

The blanket form is regretted everywhere it exists — Rust lints it, Python’s style guide forbids it, Elm discourages it for libraries — and that settles use pool::* on its own.

The selected form is the real question, and it is refused on a different ground. It is everywhere (Java, Python, Rust, Haskell, Elm, OCaml) and withdrawn nowhere, so it is not a mistake and this record does not claim it is one. Qualified-only is also sustained: Go has had no import selection for fifteen years with no serious movement to add it. One community that had both converged away from the convenient form — Haskell’s practice moved toward explicit lists and qualified names, for the reason this decision is about.

So general experience does not decide it: both work. What decides it is a rule this language has already applied to itself — one form, not two spellings of it (ADR-041 D1 withdrew the automatic lambda argument names on exactly that ground). A selected import creates that situation for every name in the language: the same type spelled pool::Conn here and Conn there, both correct. Having just paid to remove one such pair, the language should not introduce another across its whole surface.

And the refusal is not irreversible, which is why it can be firm. Adding a selected form later breaks no program: everything written with a prefix stays valid. If a program of real size shows the prefix to be a genuine cost rather than a predicted one, the form can be added then, with evidence instead of a guess. What is not deferred is the decision — a question left open is one that every file written in the meantime has to live with unanswered.

D3 — use pool as p shortens the prefix, once, in one place.

The one thing this answer adds rather than refuses, and it is what makes D2 affordable. It also settles a collision: two libraries that both want to be http are the consumer’s to name apart, which is authority the consumer needs anyway (ADR-047 D2’s manifest key rests on it).

D4 — A prefix must be introduced.

pool::Conn without use pool is refused, so that a file still lists what it depends on at the top. Without this the use line becomes optional decoration and the dependency list stops being one.

D5 — One name per file.

Two units that end up under the same name — by alias, or by collision — is an error rather than a rule about which of them wins. A rule about which wins is a rule a reader has to know to read a line.

3. What this does not answer

Re-export, and that is deliberate: there is no way here for a unit to offer a name that another unit declares. ADR-047 D1 removes the question rather than answering it — inside a package the names already share a space, so there is nothing to re-offer.

4. Consequences

Part I 9.1 gains the alias and the two rules, and the refusals are three new diagnostics rather than parse errors: a message that names the way out is the whole difference between a rule and an obstacle (Part III, C.2).

A qualified name reaches the emitter as written, which is what makes this cheap: the whole path is interned as one name, so pool::Conn in Nikaia is pool::Conn in Rust and nothing resolved it (ADR-011 D2).

The cost is a prefix at every written name, and it lands where this language already asks for a type: a signature, and the annotated let that is the only place sharing begins (Part I, 6.2). What is bought is that a reader of any line knows where every name in it comes from without consulting the top of the file, and that no edit elsewhere changes what an already-written line means.

5. What is built

D1, D4 and D5, and one of D2’s three forms. What made them reachable is ADR-047 D2: until a package could be depended on, every use but std’s was refused for the simpler reason that there was nothing to name.

D1’s qualified form. A qualified type name resolves to the type its package declares, a qualified struct literal (http::Request(path: "/x")) parses and is checked field by field, and a qualified call resolves against the package’s own ledger entries.

D2, all three forms. A path that is not std’s — use http::server — is refused with “use names one package and brings no name in”, and the message names use http and the qualified spelling. The braced and glob forms get their own sentences, in the grammar, beside the fn: … refusal ADR-022 already put there — both are in every language that has them, so a reader will write one.

Two things that arm needed. It comes first in the rule, because the plain arm matches use http and leaves the rest to fail as the next item — at the same place, with nothing to say. And it peeks the brace rather than consuming it: a fail reports at the position it runs at, and consuming the brace first put that after the implicit whitespace, which had the caret under the line below.

D4, a prefix must be introduced. use http where no dependency is called http is refused, and the message says where a package is declared — which is D4 read from the other side: an introduction that names nothing.

D5, one name per file. Two use lines under one name is refused, and it is checked before reachability: a name written twice is a fact about the file and says nothing about whether either resolves, so asking about reachability first would answer a file with two unknown names by naming one of them.

D3, the alias. use http as h shortens the prefix, and a call, a type and a struct literal all reach through it.

It is resolved away rather than emitted as a Rust use … as …. Every file’s items are in one crate root (ADR-047 D1), so a Rust alias there would be the whole program’s name for the package rather than this file’s. The resolving happens in one place — Parsed::unaliased, on the way to every name — because the alternative was the same lookup at fourteen call sites in three modules, where the fifteenth would have been the one that forgot. It is on Parsed because an alias is a name local to one file, so resolving it is not name resolution in ADR-011 D2’s sense: it is reading the file’s own dictionary, and the dictionary is part of what was parsed.

A diagnostic names the package, not the alias. “http::Request.method is private to http” where the file wrote h::Request: the type’s name is the package’s whatever one file calls it, and it is the package that keeps a name private. The use line that connects the two is at the top of the same file.

And D5 counts the alias, which is what makes D3 and D5 one rule rather than two: the alias is the name this file introduces, so use http as h beside use https as h is exactly the collision D3 exists to let a consumer fix.

This site is open source. Improve this page.