Until now, most examples have fit in one file or one REPL session. Real programs do not. They are split into multiple classes, multiple files, and clear boundaries between parts of the system.
In Nex, the main tool for bringing code from another Nex file into the current program is intern.
Splitting a program across files is not mainly about size; it is about design. Put code in a separate file when:
A Bank_Account class, a Transaction class, and a Report_Printer class should not live in one file merely because they all belong to the same application. They are different concepts with different reasons to change.
intern StatementNex loads classes from other files with intern:
intern math/Calculator
This means: find the Nex file for Calculator under the math path and make that class available in the current program.
An alias may also be given:
intern math/Calculator as Calc
That allows the imported class to be referred to locally as Calc.
intern Resolves Filesintern searches in this order:
~/.nex/depsFor a path-qualified intern such as:
intern net/Tcp_Socket
Nex tries these local layouts under each search root:
Tcp_Socket.nex
tcp_socket.nex
lib/net/Tcp_Socket.nex
lib/net/tcp_socket.nex
lib/net/src/Tcp_Socket.nex
lib/net/src/tcp_socket.nex
Then it tries the same path forms under:
~/.nex/deps
So all of these are valid examples:
./lib/net/tcp_socket.nex
/some/project/lib/net/Tcp_Socket.nex
~/.nex/deps/net/tcp_socket.nex
~/.nex/deps/net/src/Tcp_Socket.nex
Exact-case filenames are checked first. If they are not found, Nex falls back to a lowercase filename such as tcp_socket.nex.
Suppose math/Counter.nex contains:
class Counter
create
make() do
count := 0
end
feature
count: Integer
increment() do
count := count + 1
end
value(): Integer do
result := count
end
end
Another file may use it with:
intern math/Counter
class Main
create
make() do
let c := create Counter.make
c.increment
c.increment
print(c.value)
end
end
The point is not merely that the code compiles. The point is that Counter now has its own module boundary. It can be read, tested, and reused on its own.
The path in intern path/Class is not only a way to find a file. It also becomes that class's namespace. Two libraries can each define a class named Account without conflict, right up until a single program interns both of them:
intern finance/Account
intern billing/Account
let a := create Account.make(1)
Nex does not guess which Account is meant here, and it does not let whichever library happened to load first silently win. It rejects the program at compile time:
Ambiguous reference to 'Account': interned from billing.Account and finance.Account.
Reference one directly by its qualified name (e.g. billing.Account), or rename it
on the way in with a path-qualified `intern ... as`.
The fix is to write the class's full name — the same path/Class form used on the intern line — anywhere the class name would otherwise appear: in a type annotation, in create, in inherit, in a match clause.
intern finance/Account
intern billing/Account
let a: finance/Account := create finance/Account.make(1)
let b: billing/Account := create billing/Account.make(2)
print(a.balance)
print(b.id)
Both classes are now usable in the same program, each referred to unambiguously by its own qualified name. Two things are worth noting about how this behaves:
intern paths happen to lead to the same file — two libraries that both depend on a third and each intern it — that is not a collision. It is one class, reachable two ways.A bare, unqualified name — no path at all, like a plain intern Logger — never collides with anything else brought in the same way, so this only comes up once a program starts interning path-qualified libraries.
Aliases are useful when:
Example:
intern geometry/Long_Polygon_Name as Polygon
Now:
let p := create Polygon.with_sides(5)
Use aliases sparingly. The goal is clarity, not abbreviation for its own sake.
A path-qualified alias is also a second way out of the ambiguity in the previous section — rename one side of a collision (or both) on the way in, and each alias refers unambiguously to its own class from then on:
intern finance/Account
intern billing/Account as Billing_Account
let a := create Account.make(1)
let b := create Billing_Account.make(2)
Here the bare name Account still means finance's Account — nothing about it changed — while Billing_Account reaches billing's class directly, without ever touching the name it collides on. Aliasing only helps this way when the intern being renamed is path-qualified; aliasing a bare, unpathed intern is purely for shortening or readability, since there is no path to disambiguate with.
A good file boundary usually matches a good class boundary. As a rough rule:
Ask of every file:
Files are design documents as much as storage containers.
These belong together:
Stack class and helper routines whose only purpose is to support the stackThese do not:
Stack, Customer, and Image_Loader in the same fileIf you feel tempted to group code by when it was written rather than by what it means, stop and redesign.
The best modules reduce mental load. A reader opening a file should have a good guess what they are about to find.
Contracts become even more important once code is split across files. When a class is used from another module, the reader of the calling code should not have to open the original file to learn its basic obligations and guarantees; good preconditions, postconditions, and invariants are what make module boundaries trustworthy.
In a multi-file program, the contract is often the first and most important documentation of a class.
Imagine a program with these concepts:
TransactionAccountLedger_ReportA clean structure would be:
finance/Transaction.nex
class Transaction
create
make(d: String, a: Real) do
description := d
amount := a
end
feature
description: String
amount: Real
end
finance/Account.nex
intern finance/Transaction
class Account
create
make(name: String) do
owner := name
entries := []
end
feature
owner: String
entries: Array[Transaction]
add_entry(t: Transaction) do
entries.add(t)
end
balance(): Real do
result := 0.0
across entries as entry do
result := result + entry.amount
end
end
end
finance/Ledger_Report.nex
intern finance/Account
class Ledger_Report
feature
print_balance(a: Account) do
print(a.owner + ": " + a.balance.to_string)
end
end
Each file has one job. The design is visible in the file structure itself.
intern path/Class_Name to load Nex classes from other filespath in intern path/Class is also the class's namespace: two interned classes with the same bare name are a compile-time ambiguous reference error, never a silent guesspath/Class) directly, or by giving one side a path-qualified alias with asas when a local alias improves clarity, even without a collision1. Split a simple earlier example into two files: one defining a class and one using it with intern.
2. Take a class that currently does too much and divide it into two classes in two files. Explain why the new boundary is better.
3. Write a short example using intern ... as ... and show why the alias is helpful.
4. Sketch a directory layout for a small address-book program with classes Contact, Address_Book, and Csv_Exporter.
5. Write two small libraries that each define a class with the same name, then intern both into one program. Show two different ways to use both classes: a qualified reference, and an alias.
6.* Choose a chapter 26-sized program idea of your own. Before writing any code, propose the file structure and justify each file in one sentence.