package provider

  1. Overview
  2. Docs

Provider - Dynamic Dispatch with Traits.

A provider is a construct that implements a set of functionality that a library typically needs in order to provide certain functionality to a end user.

The module is divided into several submodules:

  • Trait: To identify functionality.
  • Binding: Associates a Trait with an implementation for it.
  • Private: Used for testing purposes.

The main module provides the following functionalities:

  • Implement and lookup traits.
  • Manages the set of Traits that a provider implements.

This module is inspired by the Eio.Resource module and provides a way to parametrize code when a library either doesn't want to or can't commit to a specific implementation.

module Trait : sig ... end
module Binding : sig ... end

Implementing Traits

val implement : ('t, 'module_type, _) Trait.t -> impl:'module_type -> 't Binding.t

implement trait ~impl:(module Impl) says to implement trait with Impl. The module Impl provided must have the right module type as specified by the type of trait.

The tags associated with the trait are ignored at this stage. The handling of the tags happens at the provider building stage, not at the granularity of each Trait. This means that the implement function focuses solely on creating the implementation, without considering the tags that indicate which Traits are supported by the provider.

Building providers

type ('t, -'tags) t

A provider is essentially a collection of bindings, associating each Trait it contains with an implementation for it. Each Trait provides a specific functionality (one Trait implementation = one first-class module with type t = 't).

  • 't is the internal state of the provider.
  • 'tags indicate which functionality are supported by the provider. It is a phantom type using polymorphic variants. To give an example, in the tests for this library, we have two modules defining each their own tag:
module Directory_reader = struct
  type tag = [ `Directory_reader ]
end

module File_reader = struct
  type tag = [ `File_reader ]
end

Then, the type of a provider whose internal type is state, implementing both Traits would be:

(state, [ Directory_reader.tag | File_reader.tag ]) Provider.t
val make : 't Binding.t list -> ('t, _) t

make bindings create a new provider from a list of bindings. It only keeps the last implementation supplied for each Trait, from left to right. This means that the resulting provider will not contain any duplicate Traits, and the order of the bindings in the input list can affect its contents.

val bindings : ('t, _) t -> 't Binding.t list

bindings t returns a list of bindings with the Traits that the provider t supports. See also extend.

val extend : ('t, _) t -> with_:'t Binding.t list -> ('t, _) t

extend t ~with_ extends the provider t and returns a new provider that includes both the original and additional bindings. The resulting provider only contains the last occurrence of each Trait, prioritizing the rightmost elements in the combined list bindings t @ with_.

Lookup

A lookup operation is used to retrieve the implementation of a specific Trait implementation from a provider.

val is_empty : ('t, _) t -> bool

is_empty t checks if a provider t implements any Traits. An empty provider may be created using make []. It will cause any lookup operation to fail. It can be useful for initializing data structures or providing a base case for algorithms that process providers.

val lookup : ('t, 'tags) t -> trait:('t, 'implementation, 'tags) Trait.t -> 'implementation

lookup t ~trait retrieves the implementation for a given trait from a provider.

If the provider has correctly exported their implementation using the appropriate tags, the compiler will ensure that this function does not fail in user code (a failure of this function would typically indicate a programming error in the provider's setup).

In the rare case where a provider has not correctly exported the tags of their implementation, this function will raise an internal exception. The exception is not exported, because it is not raised assuming a correct usage of the library.

You can find examples of incorrect usage in the tests of this library (e.g. "test__invalid_tags.ml").

val lookup_opt : ('t, _) t -> trait:('t, 'implementation, _) Trait.t -> 'implementation option

lookup_opt t ~trait returns the implementation of the trait if present (Some implementation) or indicates that the Trait is not implemented (None).

This is particularly useful in scenarios where a part of a program needs to adapt behavior at runtime based on whether certain functionalities are available or not.

val implements : ('t, _) t -> trait:('t, _, _) Trait.t -> bool

implements t ~trait says wether a provider implements a Trait. This is true iif lookup_opt t ~trait returns Some _.

type -'tags packed =
  1. | T : {
    1. t : 't;
    2. provider : ('t, 'tags) t;
    } -> 'tags packed

A packed provider is a pair of a value and a set of Traits that a provider implements on that value. This is an OCaml value that behaves roughly like an object, but isn't one.

module Private : sig ... end

This module is exported for testing purposes only.

OCaml

Innovation. Community. Security.