Below,
for historical/posterity purposes only, is some informal notes to myself from a sketch of where I was going:
// Prior art with inspiring bits:
// * Typesafe Config (for distinction between null and missing, ConfigBeanFactory etc.)
// * Dropwizard Config (for POJO-first and user-designs-config mentality; documentation is
// solved as the "schema" is just the config object itself and the documentation is just
// its javadoc; no intermediate map-like structures or scalarization)
// * java.util.ResourceBundle (for distinction (clumsy) between null and missing and
// including other implicit coordinates as part of an address (Locale.getDefault())
// * MicroProfile Config, mostly just for flat keys
// * javax.naming.Context (and object factories etc.; look up a Thing at an address;
// it's built and made for you out of other parts; distinguishes between null and
// missing, features rich addresses (names +
// hash tables-representing-environmental-coordinates))
// Things I need to bear in mind:
// * one goal seems to be to allow the config system to configure itself; don't prevent this
// * no DI in the API, C or otherwise :-)
// * focus hard on class developer, not application; address-space comes to the forefront
// * address collisions: developer 1 creates address A; developer 2 creates address B;
// A == B in some abstract way but they "target" different things; they're now combined
// into an app; kaboom; how do we deal with this? need transliteration
// What's the main use case? Class developer asks configuration system to load a very
// particular object of a specific type. System responds with exactly the object asked for,
// or an object that is suitable, or indicates that the object isn't there, permanently
// or temporarily. Addresses are written by class developers,
// but used by applications, which combine objectspaces from multiple class developers.
// Open questions early on: if developer asks for a CharSequence, can String be returned?
// Seems like subtyping like this should work, so we're talking runtime type resolution.
// If user asks for a List<String>...? A FancyObject<Map<String, List<String>>>...?
// (Config is, of course, DI without the I, so all the same problems exist)
// Loading:
// Get a loader thing, as simply as possible:
Loader loader = Loader.loader();
// Ideally, since this is config, loader() will *start* by using something easy like
// ServiceLoader, but maybe also has built-in post-bootstrapping use-the-loader-to-load-
// the-real-loader machinery? So maybe a custom Loader implementation can load another
// Loader implementation using some rich facility of its own; in any event, this is and
// should be a one-liner from the class developer standpoint.
// First up: value absence, presence and determinism:
//
// Let's posit OptionalSupplier<T>:
//
// OptionalSupplier<T> looks like an Optional<T>, let's say, but is deliberately not one,
// and its Optional-like methods do not trigger on null, but on *absence*,
// as indicated by get()'s throwing of a suitable exception, following conventions
// established by java.util.ResourceBundle, javax.naming.Context.lookup(),
// and others. An OptionalSupplier<T> as used here represents an optimized production
// facility for a T-typed value. Optimized: because loader.load() may have scanned 347
// sub-providers of which only five were even remotely possible for producing this
// kind of value; the five would be encapsulated/represented by this supplier, not the
// other 342:
OptionalSupplier<String> greetingSupplier = loader.load(someKindOfAddress);
// Punt for now on registering for changes, but maybe:
// loader.load(someKindOfAddress, somethingThatLooksLikeJNDIEventContext);
// (Maybe Loader itself is also an OptionalSupplier? then you could do (in JNDI style):)
// Loader<String> greetingSupplier = loader.load(someKindOfAddress);
// Not *much* different from:
Optional<String> greetingOptional = Config.getOptionalValue("someName", String.class);
// ...but! Different nonetheless! And **the differences are critical**.
// OptionalSupplier.get() deliberately may return null or anything else at all times as a
// valid value.
// So its orElse() etc. methods trigger on value *absence*, not value *nullness*.
// If get() throws a NoSuchElementException, for example, that would indicate absence.
// If it returns null, greeting gets null:
String greeting = greetingSupplier.get(); // could return null
// Let's say you want a default value for *absence* but not necessarily null.
// Here, the orElse() method doesn't "trigger" if greetingSupplier's get() method
// returns null, but does "trigger" if get() throws, say, a NoSuchElementException:
greeting = greetingSupplier.orElse("Hello!");
// At any point you can turn an OptionalSupplier into an Optional. Maybe you
// don't *want* to distinguish between null and absence in your application:
greeting = greetingSupplier.optional().orElse("Hello!");
// OK, what happens if you call get() twice?
// Let's say you want to know if the value's absence or presence is permanent:
if (greetingSupplier.isDeterministic()) {
// Whatever get() does, it must always. do. This might be a null value,
// a non-null value, or a NoSuchElementException (or the like).
// Now you know you can cache the value or whatever; you never
// have to call get() again
} else {
// greetingSupplier.get() might return different values, or might throw
// to indicate values becoming absent; null is a valid value in all cases
}
// Back to loader.load(someKindOfAddress): what's the address?
//
// someKindOfAddress is effectively a non-specific flat pointer. It would be a combination
// of type, discriminator (name), and application-supplied coordinates (instead of
// just name and class, for example). It is flat, and there is no hierarchy (as with
// some JAX-RS paths, for example, where attempting to get an intermediate "node" results
// in a "missing" exception).
//
// non-specific: because you are asking for something very specific, but the config machine
// may only have something very general. "I want the greeting for the test environment
// and for the German locale and the one to be used on Thursdays" "Sorry, all I have is
// an English greeting; it's not earmarked for test or German or anything else" Or of
// course the thing it points to may be absent, temporarily or deterministically.
//
// A rich address lets you do:
//
// "Get me the most suitable Frobnicator<List<String>> for where my application
// is currently running and not just any Hairball<String> but the one
// {waves hands} located at/described by/discriminated by/pointed to by
// the combination of "goop", whatever that means, and any application coordinates that
// may exist:"
Frobnicator<List<String>> fancyThing =
loader.load(new Address<Frobnicator<List<String>>>("goop",
Loader.getAppCoordinates(),
otherDiscriminators()));
// The Loader interface can be implemented in one class that handles all loading requests
// itself, or with multiple mediated back ends, or with a central class and an SPI
// belonging to that central class, or....
// Everything else happens behind the scenes, and none of these affect the API above:
// * conversion
// * one vs. many backends
// * object binding
// * service discovery/provider loading/loader loading