Code That Says Exactly What It Means

Peter C peterc at gmail.com
Sun Oct 26 06:33:11 UTC 2025


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");
}



More information about the Digitalmars-d mailing list