Functional Core, Imperative Shell implemented with Finite State Transducers

In a previous article we looked at Finite State Transducers (FSTs). To quickly recap, a Finite State Transducer is a type of Finite State Machine that also produces output. This technique for structuring your code can work well with many modern programming paradigms and patterns. In this article, we’re going to see how it fits in with the Functional Core, Imperative Shell (FCIS) pattern.

Functional Core, Imperative Shell and FSTs

Functional Core, Imperative Shell separates your program into two parts:

  1. A core of pure functional code which encapsulates the domain logic of your program and declaratively describes the effects to run against external systems.
  2. A shell of imperative code which interacts with the outside world.

This looks something like this diagram:

Separating your program’s code like this gives you several advantages. You can test your logic easily and in isolation to make sure that your program makes the correct decisions about how to handle various inputs in various states. You can change the external systems you use, such as storing data in a different database or using a different authentication provider, without rewriting your entire program. And perhaps some more benefits along these same lines.

One way to implement the Functional Core is to use a pure function with a signature like Input => Output. If your logic is stateful, then that signature might be (State, Input) => (State, Output). Hey that looks familiar – that’s the signature of an FST.

One other thing to note is the direction of the dependency. The Imperative Shell depends on the Functional Core, so the Imperative Shell knows about the Functional Core, but the Functional Core knows nothing about the Imperative Shell. This is a great property for domain logic to have. The fewer things it knows about, the fewer sources of change there are that force it to change. Only changes to the domain should require any changes to the Functional Core. Many domains change at a much slower rate than technologies, and if your domain is one of them, your functional core should be a stable, hardy, long-lived section of code.

An Example in TypeScript

Consider the following situation. You have a UI for displaying some data. There is a button to fetch data from a server and it gets displayed in a list. Users commonly request large amounts of data or use complex queries, so it can take a while to fetch data (wait times > 10 seconds are common) and each fetch is relatively expensive. We need to provide visual feedback to the user about the status of their query and prevent the user from spamming the button and polluting the server with extra queries.

Let’s model this problem to fit with a Functional Core, Imperative Shell design. The external systems for our program are the backend service and the UI.

The Imperative Shell consists of the data fetching API, the UI rendering and event handling APIs, and the storage of the state of our fetching operation.

The Functional Core consists of the meaningful user actions we need to process, such as GetMyData, responses from the backend, such as FetchDataSuccess or FetchDataFailure, the imperative effects to run, such as SendFetchRequest or RenderData, the possible states we can be in, such as Idle or Fetching and the logic to determine which effects to run based on the current state and the input.

We will model the Input, Output, and State types using Algebraic Data Types built on top of Discriminated Unions.

Let’s see what the code might look like for our Functional Core:

type State =
  | Idle
  | Fetching

type Input =
  | FetchData
  | FetchDataSuccess
  | FetchDataFailure
  
type Output =
  | SendFetchRequest
  | PublishData
  | PublishFailure
  | LogError
  | NilOutput // represent 'undefined' more concretely

function initialState(): State {
  return idle();
}

function transition(state: State, input: Input): [State, Output] {
  switch (state.kind) {
    case 'Idle':
      return handleIdle(state, input);
    case 'Fetching':
      return handleFetching(state, input);
    default:
      assertNever(state);
  }
}

function handleIdle(state: Idle, input: Input): [State, Output] {
  switch (input.kind) {
    case 'FetchData':
      return [fetching(), sendFetchRequest(input.query)]
    case 'FetchDataSuccess':
    case 'FetchDataFailure':
      // we got a server response when we weren't expecting it.
      // this probably indicates an error somewhere.
      return [state, logError('unexpected response')];
    default:
      assertNever(input);
  }
}

function handleFetching(state: Fetching, input: Input): [State, Output] {
  switch (input.kind) {
    case 'FetchData':
      // we are already fetching, we do not need to fetch again
      return [state, nilOutput()];
    case 'FetchDataSuccess':
      return [idle(), publishData(input.data)];
    case 'FetchDataFailure':
      return [idle(), publishFailure(input.error)];
    default:
      assertNever(input);
  }
}

Pretty straightforward. This can be written more succinctly in languages that support tuple pattern matching. For example, in Scala you might have something like this:

def transition(state: State, input: Input): (State, Option[Output]) =
  (state, input) match {
    case (Idle(), FetchData(query)) =>
      (Fetching(), SendFetchRequest(query))
    case (Fetching(), FetchData(_)) =>
      (state, None)
    case (Fetching(), FetchDataSuccess(data)) =>
      (Idle(), PublishData(data))
    case (Fetching(), FetchDataFailure(error)) =>
      (Idle(), PublishFailure(error))
    case (_, _) =>
      // anything else is unexpected and should be logged as an error
      (state, LogError('unexpected transition')
  }

Let’s see what the code might look like for our Imperative Shell:

function createImperativeShell(
  backendApi: BackendApi, // maybe just 'fetch', maybe GraphQL, etc
  ui: UiApi,
  logger: Logger,
) {
  // state storage is just in-memory
  let state = initialState();

  // feed inputs into functional core
  const handleInput = (input: Input) => {
    const [nextState, output] = transition(state, input);

    // can also add tracing here to log inputs, outputs, state transitions

    state = nextState;
    handleOutput(output);
  }

  // run effects against external systems
  const handleOutput = (output: Output) => {
    switch (output.kind) {
      case 'SendFetchRequest':
        // visual feedback to user that we're fetching
        ui.showStatusSpinner();

        // map api responses to domain actions
        backendApi.fetch(output.query)
          .then(data => handleInput(fetchDataSuccess(data))
          .catch(err => handleInput(fetchDataFailure(err));
      case 'PublishData':
        ui.renderData(output.data);
      case 'PublishFailure':
        ui.renderFailure(output.error);
      case 'LogError':
        logger.error(output.error);
      case 'NilOutput':
        // nothing to do
      default:
        assertNever(output);
    }
  }

  // map ui events to domain actions
  ui.onFetchButtonClicked(() => {
    handleInput(fetchData(ui.currentQuery());
  })
}

The implementation details of the Imperative Shell are going to vary a lot depending on what kind of external APIs you need to bind to. Hooking into a React-style UI looks different from hooking into a jquery-style UI. Hooking into a fetch-style API looks different from hooking into an asynchronous-messaging-style API (like a websocket).

Conclusion

Functional Core, Imperative Shell is a great pattern for organizing the code in your program. The FST pattern is a nice way to implement the Functional Core. FSTs put the focus on your domain logic and are easy to plug into other code due to their lack of dependencies on external technologies. They can be implemented in a variety of programming languages and often look very similar even in different languages.