NAME
App::Ground - Ground your application.
DESCRIPTION
In circuit design, "ground" (GND) is a shared reference node (often called "0V") and a current return path. It is convenient because you can connect to it from many places, but it is not magic: real circuits may have multiple grounds, local grounds, and bad grounding can create noise or loops.
App::Ground uses the same metaphor for programming: it provides a small set of modules that act like a "ground" for infrastructure dependencies (logging, configuration, application handle, optional request context). This reduces parameter plumbing, but it should be used for infrastructure, not business data.
This is the classic trade-off of ambient authority vs dependency injection: globals are convenient, DI is explicit.
EXAMPLES IN OTHER LANGUAGES
JavaScript / Node.js
Helpers can use the global console and environment without receiving them as arguments.
function deepCall() {
console.log('hello', process.env.PORT);
}
Rust
After the logger backend is initialized, helpers use log::info! without receiving a logger.
fn deep_call() {
log::info!("hello");
}
Go
Go's standard logger is shared across packages, so helpers can log without receiving a logger.
import "log"
func deepCall() {
log.Print("hello")
}
Node.js (AsyncLocalStorage)
A shared AsyncLocalStorage instance gives helpers access to context specific to their asynchronous call chain.
const { AsyncLocalStorage } = require('node:async_hooks');
const context = new AsyncLocalStorage();
function deepCall() {
console.log(context.getStore().requestId);
}
context.run({ requestId: 'req-123' }, () => setImmediate(deepCall));
Java
Within an application class, a static SLF4J logger uses the shared logging backend without being passed to each method.
private static final Logger LOG = LoggerFactory.getLogger(Example.class);
void deepCall() {
LOG.info("hello");
}
USAGE (THIS DISTRO)
This distribution ships several tiny "ground" modules.
A - Application
A is a place for your application's bootstrap code, giving scripts a shared environment through one import.
use A; # Load the setup defined in your application's lib/A.pm.
C - Context
C gives deep code access to the application and its current context, as well as the application's database and configuration.
C::app( $app ); # Register the application at startup.
my $db = C::db;
my $cfg = C::config;
sub handler {
my( $c ) = @_;
local $C::C = $c;
deep_call();
$c->render( text => 'ok' );
}
sub deep_call {
my $c = C::C() or return;
# ...
}
See C for accessors and the tutorial below for initialization.
I - Iterator
I lets tests supply in-memory rows to code that uses first and next, without querying a database.
my $rows = I->new([ { name => 'Alice' }, { name => 'Bob' } ]);
my $first = $rows->first; # { name => 'Alice' }
my $next = $rows->next; # { name => 'Bob' }
L - Logging
L lets you choose a logging backend at startup and use the same logging calls throughout the application.
$L::logger = sub { $app->log };
L::info "Application ready\n";
M - Memory / Mapping
M keeps shared lookup tables available wherever they are needed.
my $month = $M::mon2num->{ jan }; # "01"
S - Singleton / Shared
S is a namespace for services or state your application initializes once and shares across its code.
$S::service = $service; # Your application supplies this shared service.
$S::service->perform_action();
T - Time
T groups date and time helpers, including adding months with shorter months handled automatically.
my $date = T::nmonth '2024-01-31', 1; # next month
my $str = T::fd $date; # format date "2024-02-29"
U - Utilities
U provides a set of stable utility functions that are safe to use as global helpers from anywhere in an initialized application; see U for more examples.
U::ewarn 'Import failed', 'Could not read the input file'; # Log and email.
U::email $to, $subject, $body, \@attachments;
my $str = U::round $number;
TUTORIAL
Initialize once, then use shared infrastructure
Initialize the application before using utilities: load configuration, set up logging handlers and database connections, then register the application. The application and its infrastructure are intended to be effective singletons within a process. Treat configuration as stable after startup; utility callers reuse these facilities rather than constructing or replacing them on each call.
use C;
use L;
use U;
my $cfg = C::load './app.conf';
# Construct/configure $app with $cfg, its logger and database connections.
# Its config method must expose $cfg; its mode selects development behavior.
C::app( $app );
$L::logger = sub { $app->log };
# Application code can now use the initialized infrastructure.
L::info "Application ready\n";
U::email 'user@example.com', 'Welcome', "Hello\n";
my $amount = U::round 5.6777, 2; # "5.68"
When an application is registered, C::config reads $app->config. C::load supplies the fallback configuration when no application is registered; it does not automatically configure the application object.
Request context and deferred work
Set $C::C with local at the request boundary so helpers can use C::C(). Dynamic scoping does not automatically cross asynchronous or promise boundaries; capture the context and re-localize it in a deferred callback.
Depend on outcomes
The configured infrastructure may differ between applications, but callers should be able to rely on a clear action and expected result without worrying about which implementation was configured or what happens inside it.
Logging: whichever logger you configure, call L::info and a string is logged.
Email: whichever mail server you configure for sending, call U::email and an email is sent to the recipient.
Initialize these services to provide the expected behavior. Their shared configuration lets callers use the same operations throughout the application. U should not introduce persistent state of its own: call -> result or immediate effect.
AUTHOR
Eugen Konkov