---
# System prepended metadata

title: Just enough dependency injection

---

# 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