Peter C 
| Among D developers, the idea of adding a new visibility attribute like scopeprivate often meets with scepticism. After all, D already has private for module-level encapsulation and package for sharing across sibling modules. And if you really want to lock a class down, you can always put it in its own module. So why bother?
The problem is that these existing tools force you into coarse choices. If helpers and tests live in the same module, they see everything. If you isolate the class in its own module, they see nothing. And while package is useful for widening access across a package hierarchy, it doesn't let you narrow visibility inside a module. Rather, package widens visibility, across a whole package hierarchy. That's not the same as narrowing it down. In practice, this means you can't express a very common design intent: "helpers in the same module should see some internals, but not the most sensitive ones."
That's where a 'scopeprivate' comes in. It introduces a missing rung on the visibility ladder: type-only access. With it, you could mark fields like Account.id or Account.owner as private or package, so persistence and logging helpers can use them, while marking fields like Account.balance or Account.authToken as scopeprivate, so only the class itself can touch them. The compiler enforces this boundary, preventing accidental leaks in logs, persistence, or tests. You no longer need to split every class into its own module just to achieve stricter encapsulation, and you no longer need to rely on convention or code review to stop sensitive data from slipping out.
In other words, scopeprivate doesn't make the impossible possible - you can already hack around with module boundaries and package. What it does is make disciplined program design, explicit and enforceable. It lets you express intent directly at the declaration site, keeps related code together without sacrificing safety, and ensures that invariants remain protected by construction.
For developers who care about clarity, maintainability, and correctness, that's not redundant - it's a meaningful step toward code that says exactly what it means.
module exampleOnly;
@safe:
private:
//import std;
import std.stdio : writeln;
import std.exception : enforce;
public class Account
{
private
{
int id;
string owner;
}
scopeprivate
{
double balance;
string authToken;
}
public this(int id, string owner, double openingBalance, string token)
{
this.id = id;
this.owner = owner;
this.balance = openingBalance;
this.authToken = token;
}
public void deposit(double amount)
{
enforce(amount > 0, "Deposit must be positive");
balance += amount;
}
public void withdraw(double amount)
{
enforce(amount > 0 && amount <= balance, "Invalid withdrawal");
balance -= amount;
}
public double getBalance() const { return balance; }
}
// Can see IDs and owners (enough to write to DB), but not balances or tokens.
public void saveToDatabase(Account a)
{
writeln("[DB] INSERT INTO accounts (id, owner) VALUES (", a.id, ", '", a.owner, "')");
// Example compiler message if you were to uncomment the lines below:
// writeln("Balance: %s", a.balance);
// Error: no property `balance` for `a` of type `finance.Account` [`balance` is not accessible here]
// writeln("AuthToken: %s", a.authToken);
// Error: no property `authToken` for `a` of type `finance.Account` [`authToken` is not accessible here]
}
// Logs can include IDs and owners for traceability, but cannot leak sensitive state.
public void logTransactionStart(Account a, string action)
{
writeln("[LOG] Account id=", a.id, " owner=", a.owner, " starting action=", action);
// Example compiler message if you were to uncomment the lines below:
//writeln("[LOG] Account balance = %s", a.balance);
// Error: no property `balance` for `a` of type `finance.Account` [`balance` is not accessible here]
//writeln("[LOG] Account authToken = %s", a.authToken);
// Error: no property `authToken` for `a` of type `finance.Account` [`authToken` is not accessible here]
}
public void logTransactionEnd(Account a, string action, bool success)
{
writeln("[LOG] Account id=", a.id, " owner=", a.owner, " finished action=", action, " status=", success ? "OK" : "FAILED");
}
// Unit tests are also subject to scopeprivate's visibility restriction.
// Compile-time visibility tests
unittest
{
auto acc = new Account(5, "Eve", 400.0, "tok5");
// Allowed:
static assert(__traits(compiles, acc.getBalance()));
// Forbidden (scopeprivate):
static assert(!__traits(compiles, acc.balance));
static assert(!__traits(compiles, acc.authToken));
}
// Constructor tests
unittest
{
auto acc = new Account(1, "Alice", 100.0, "tok");
assert(acc.getBalance() == 100.0);
}
// Deposit tests
unittest
{
import std.exception : assertThrown;
auto acc = new Account(2, "Bob", 50.0, "tok2");
acc.deposit(25.0);
assert(acc.getBalance() == 75.0);
assertThrown!Exception(acc.deposit(0));
assertThrown!Exception(acc.deposit(-10));
}
// Withdraw tests
unittest
{
import std.exception : assertThrown;
auto acc = new Account(3, "Charlie", 200.0, "tok3");
acc.withdraw(50.0);
assert(acc.getBalance() == 150.0);
assertThrown!Exception(acc.withdraw(0));
assertThrown!Exception(acc.withdraw(-5));
assertThrown!Exception(acc.withdraw(500)); // too much
}
// Logging and DB output tests
@system unittest
{
// helper to capture writeln output
string captureOutput(void delegate() dg)
{
import std.array : appender;
import std.stdio : stdout, File;
import core.stdc.stdio : fflush;
import std.string : strip;
auto buf = appender!string();
auto old = stdout;
auto f = File.tmpfile();
scope(exit) f.close();
stdout = f;
dg();
fflush(f.getFP());
f.rewind();
foreach (line; f.byLine)
buf.put(line.idup);
stdout = old;
return buf.data.strip;
}
auto acc = new Account(4, "Dana", 300.0, "tok4");
auto dbOut = captureOutput({ saveToDatabase(acc); });
assert(dbOut == "[DB] INSERT INTO accounts (id, owner) VALUES (4, 'Dana')");
auto logStart = captureOutput({ logTransactionStart(acc, "deposit"); });
assert(logStart == "[LOG] Account id=4 owner=Dana starting action=deposit");
auto logEndOk = captureOutput({ logTransactionEnd(acc, "deposit", true); });
assert(logEndOk == "[LOG] Account id=4 owner=Dana finished action=deposit status=OK");
auto logEndFail = captureOutput({ logTransactionEnd(acc, "withdraw", false); });
assert(logEndFail == "[LOG] Account id=4 owner=Dana finished action=withdraw status=FAILED");
}
|