# Just enough dependency injection Often programs and sometimes libraries need a suite of *host resources* some of which are directly available and sometimes which can only be partially emulated. Often when running tests, these external host resources are exactly those which tests need to mock or spend signifant effort setting up and tearing down. - A file system - A shell - Program input arguments and environment variables - Standard input and output streams (as distinct from standard error or logging) - An interactive prompt that allows getting input from the user - Access to a random or pseudo-random number generator (PRNG) - Network port opening APIs - Per application user preference persistence (think per-application foo.rc and history files) - A system clock - Timers APIs specifically suited for benchmarking Some of these things, like an interactive prompt, can be built on others (access to stdin and stdout) but it is convenient to treat them separately; connecting to a language's high-level prompt APIs may yield better results than trying to build one prompter to rule them all using low-level, possibly emulated APIs (for example, faking stdin/stdout using HTML elements in a webpage). This document outlines an in-Temper syntax for accessing these assuming they exist, and then walks through how they might be provided in an interpreted REPL environment and in various backends. ## A motivating example For an interactive Tic-tac-toe program in Temper, I might need access to: - A PRNG to pick moves when the computer side has no uniquely preferred move. - An interactive prompt: async access to stdout combined with access to stdin that resolves a promise when the user hits enter. - Best effort access to a high scores file. Here's how the program might request and get access to APIs. ```ts // std/system (working name) provides access to interface definitions let { PromptApi, Prng, FileSystem } = import("std/system"); // importing an interface type is recognized as connecting to a host-provided // instance of that interface, or if the interface is not associated with // a fallback, pure-Temper implementation, possibly null. let promptApi = import(PromptApi); let prng = import(Prng); let fileSystem: FileSystem | Null = import(FileSystem); ``` Maybe the file system has no implementation in the browser-side JS backend. It could bind to `null` at runtime. ## From the providers perspective The proposed `std/system` (working name) module provides some host interface types. ```ts interface IneractiveSession { public prompt(message: String): Promise<String>; // An interactive session allows prompting for a string input but also // customizing how tab-completion, history, and highlighting work, and // how to deal with exiting gracefully. ... } export interface PromptApi { public startInteraction(): InteractiveSession | Bubble; } ``` Code would then have to check if it's available before loading/saving that file, and could just not provide that feature if unavailable. Alternatively, a user-save-state affordance could be built on a file system API where available or using browser *LocalStorage* otherwise. So it'd be nice to be able to be able to act conditionally when finding implementations: - if there's a host/backend preferred implementation, provide that - if some lower-level *interface import* succeeded, provide an in-Temper implementation based on that - else return *null* A provider can export a factory function, a zero argument function that returns an instance of the type. ```ts @Provides(PromptApi) export let providePromptApi(): PromptApi | Null { ... } ``` ## How interface import and implementation export look in translation Here's how we might translate an provide and require into Java. First, a library that provides it uses Temper core APIs to register a provider. ```java package libraryA; public class SomeModuleTopLevels { public static void registerProviders() { Core.registerServiceProvider( // interface type used as key. PromptApi.class, // Translation of providePromptApi factory function. () => new PromptApiImpl(), ); } } ``` Second, that library has an entry point with a predictable name that, on class load invokes that. ```java package libraryA; /* public? */ class Init_LibraryA { static { SomeModuleTopLevels.LibraryA__ServiceProviders(); // others } public static void ensureProvidedServicesLoaded() { // Guarantees static blocks run per docs.oracle.com/javase/specs/jls/se9/html/jls-12.html#jls-12.4.1 } } ``` A class doesn't need to be public to be initializable across pacakges, because there are [reflection tricks](https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#forName-java.lang.String-boolean-java.lang.ClassLoader-) but it can be simpler if we can just invoke a method like `ensureProvidedServicesLoaded()` as shown for simplicity below. Third, the library that needs the service can use classloader tricks to ensure that providers are registered before they're needed. ```java package libraryB; import libraryA.Init_LibraryA; import ...PromptApi; class NeedsService { static { Init_LibrarayA.ensureProvidedServicesLoaded(); } // Top levels private static final PromptApi promptApi = Core.getService<PromptApi>(PromptApi.class); .... } ``` ## Module ordering In the Java case above, the requester of the Prompt API probably has some dependency For an implementation to be available or not, as a module loads, it's necessary to register available implementations early. We should be able to preserve the following invariant, though it might take some work: > if a library depends on libraries that provide implementations of its required interface imports, > then those get registered in time to be available. In dynamically linked languages, each library might need to export metadata about which modules in that library provide which things, so that loading a library preloads instructions to load those modules in case they're needed. In statically linked languages, we need provider definitions to precede imports. ## Test specific overrides TODO