Structuring Dyalog APL Applications with Namespaces and Explicit Arguments
Replace global variables in Dyalog APL with namespaces and explicit arguments. A practical pattern using Init functions, dependency injection, and ⎕NC/⎕NL for verification—plus the trade-offs you'll hit when scaling.
18 Dec 2025, 13:36 UTC

The Global Variable Trap
APL's interactive workspace encourages experimentation. You define a few variables, write a function that references them, and everything works—until the application grows. Six months later, you're debugging a production issue and discover that CONFIG, DB_HANDLE, and CACHE are all global variables modified by fifteen different functions. Changing one breaks three others. Testing means manually setting up the entire workspace state before each run.
The fix isn't a framework. It's a discipline: group related code into namespaces, and pass every dependency as an explicit argument. This turns fragile global state into predictable, testable transforms.
Namespaces as Module Boundaries
A Dyalog namespace is a container that holds functions, variables, and other namespaces. Create one with ##.MyModule (the ## prefix means "in the root namespace"). Inside, you define your API. From outside, you call MyModule.Function. The global workspace stays clean.
This isn't just naming hygiene. Namespaces give you:
- Encapsulation: Internal helpers stay private unless you expose them.
- Hierarchy:
MyModule.DatabaseandMyModule.Cachecan coexist without name collisions. - Inspectability:
⎕NL -9lists all namespaces;⎕NC 'MyModule.Run'confirms a function exists and is callable.
For short scripts or REPL exploration, this structure is overhead. Use it when the codebase outgrows a single screen.
Explicit Arguments Enable Testing
Global variables couple functions to a specific environment. A function that reads DB_HANDLE from the workspace can't be tested with a mock database unless you mutate the global state first—and remember to restore it afterward.
Pass the handle as an argument instead:
Query ← {handle sql ⍝ handle is explicit, not global
handle.Execute sql
}Now Query is a pure transform: same inputs, same output. In tests, pass a mock object with an Execute method. In production, pass the real connection. No workspace manipulation required.
Worked Example: A Configurable Service Namespace
Here's a pattern that combines namespaces with dependency injection. Paste this into a Dyalog session:
##.OrderService ← ⎕NS '' ⍝ create namespace in root
OrderService.Init ← {config
⍝ config: namespace with ConnectionString, Timeout, Logger fields
⎕← 'Initializing OrderService with ', config.ConnectionString
⍝ Return a configured object (namespace) with bound dependencies
svc ← ⎕NS ''
svc.Config ← config
svc.DB ← Database.Connect config.ConnectionString
svc.Logger ← config.Logger
svc
}
OrderService.GetOrder ← {svc orderId
⍝ svc is the object returned by Init
svc.Logger.Info 'Fetching order ', orderId
svc.DB.Query 'SELECT * FROM orders WHERE id=', orderId
}
OrderService.Run ← {config orderId
svc ← OrderService.Init config
OrderService.GetOrder svc orderId
}Usage:
cfg ← ⎕NS ''
cfg.ConnectionString ← 'Server=prod;Database=orders;'
cfg.Timeout ← 5000
cfg.Logger ← Logger.Create 'OrderService'
result ← OrderService.Run cfg 'ORD-1234'In tests, substitute a mock logger and a fake database:
testCfg ← ⎕NS ''
testCfg.ConnectionString ← 'mock'
testCfg.Logger ← MockLogger.Create ''
testCfg.DB ← MockDB.Create ''
OrderService.Run testCfg 'ORD-999' ⍝ runs without real infrastructureVerify the namespace exports what you expect:
⎕NC 'OrderService.Run' ⍝ returns 3 (function) if visible
⎕NL -9 ⍝ lists OrderService among root namespacesTrade-offs and Limitations
Namespaces add indirection. For a 50-line utility, the extra structure slows you down more than it helps. Adopt it when you feel the pain of global collisions or untestable code—not before.
Circular dependencies between namespaces are a real hazard. If OrderService references InventoryService and vice versa at initialization time, you'll hit initialization order bugs that only appear in certain load sequences. Break cycles by passing dependencies as arguments (as shown above) rather than referencing sibling namespaces at parse time.
If you later parallelize with isolates (⎕NEW or isolate), each isolate gets its own copy of namespace-local state. Shared mutable state requires explicit synchronization—namespaces don't solve this automatically.
What to Do Next
Pick one module in your workspace that touches globals. Wrap it in a namespace, add an Init function that accepts a config object, and thread that object through the call chain. Run ⎕NC 'YourModule.Run' to confirm the entry point is visible. Then write a test that passes a mock config. If the test runs without a live database, the pattern works.
Clean up with )ERASE YourModule when done experimenting—verify no globals leaked.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.