Humainary Observers 101

Introduction

The Humainary Observers API is an open and extensible observatory framework for constructing, composing, and coordinating observers and their state inspection models on top of other observability instruments provided by other projects under the Humainary observability toolkit initiative. This post will detail each of the interfaces available in this library, which in many cases extend foundational interfaces in the Substrates project. If you are not familiar with the Substrates interfaces, such as Context and Instrument, please read the introductory posts here.


The Observer Interface

The Observer interface extends the Instrument interface within the Substrates project. Like all instruments libraries built on top of the Substrates project, an Observer is created via a Context and associated with a Name. The Name is used to identify the Observer within the Context and identify what is observed in the underlying state space (data source) by all instruments of a particular Context. Basically, there is a named observer for every observable object (<O>), which can be as simple as a keyed value within a map-like structure or another Instrument of another type or another Observer within a different Context. Composability (and chaining) is at the core of the design of this library interface.

The Observer interface defines a single method – observe. The method generates and publishes an observation result (<R>) depending on whether a change has occurred in the underlying state inspected since the last calling.

Though it looks at face value as a pull mechanism, the underlying state can be pushed via a Subscription when the data source extends the Source interface within the Substrates project. This characteristic of the library design is covered in greater detail in a follow-up post.


The Optic Interface

An Observer needs an optical device to capture an observation value (<V>) and compose an observation result (<R>) from the underlying observable state space for an Observer to function. This is the job of the Optic interface within the Observers API. The Optic interface comprises three interfaces defined within the API – Bootstrap, Lens, and Operant. When a Context is created, it is provided with a data source (state space) instance and an Optic instance from which all observers will capture and collect state.

For the most part, an instance of the Optic interface is stateless, as each Observer will use the same instance when instructed to.

Contextual state management, at the level of an individual Observer, is supported by the Closure interface that is shared and passed across methods defined in the above-mentioned interfaces per Observer instance. This is discussed further later in the post.


The Bootstrap Interface

The purpose of the Bootstrap interface is to initialize the state management for each Observer instance before the first state inspection of the observable object it is mapped to by way of its Name. The initialize method is passed a Closure object (<C>) and the Name used to identify the observable within the state space managed by the Context. The Observer uses the return value of the initialize method to be the initial observation result (<R>) of an ongoing observation process. The Bootstrap interface is invoked only once per Observer instrument within a Context – it bootstraps processing state.


The Lens Interface

The Lens interface is used to capture an observed value (<V>) from the observable object (<O>). The capture method in the Lens interface is given the Closure object (<C>) and the observable object and returns an observed value. Being a functional interface, it is expected that multiple Lens instances will be chained together in transforming from an observable object to a final observed value. Other Humainary instrument libraries built on the Observers API will offer out-of-the-box implementations for common state capturing adaptors.

Let’s recap quickly. The Bootstrap interface sets up the initial processing state (<C>, <R>) for an observable object repeatedly observed by an Observer. The Lens interface continuously captures an observed value (<V>) from an observable object (<O>); much like a camera lens, it focuses on the state accessible through some public method/field of the observable object or introspection mechanism.


The Operant Interface

Finally, we arrive at the terminal stage in the observation processing pipeline of an Observer instrument – the Operant interface. An Operant is used to compose a new observation result (<R>) from a previous observation result, and a new observed value (<V>) – both processing parameters are provided to the compose method along with the Closure. The difference with the Lens interface is that the Operant has access to the previous observation result, managed and provided by the Observer and/or Context.

An Operant merges the past observation result with the present, observed value – the reductive stage in the observation processing. Much like the Lens interface, it is expected that other Humainary libraries will provide composable Operant implementations for common processing sequences. The in-development Humainary Accumlis project is one such example.


The Closure Interface

The Closure interface offers a means for an Optic to manage state across multiple observations of the same observable object. It has setter and getter methods for changing and accessing a value that Bootstrap normally initializes. The Closure interface can cache other data structures beyond the observation result, which is already managed by the runtime. It is the first value of every method signature in the Optic inherited interfaces.


In the next post, we will get to the real magic and power of the Observers API with the construction of a Context, using the Observers class utility methods that support pull-based and push-based modes of operation.

Observers.context (
  counters,
  emittance (),
  optic (
    bootstrap ( ZERO ),
    lens ( INC::equals ),
    operant ( total -> ++total )
  ),
  environment ()
);