package mirage

  1. Overview
  2. Docs

The Functoria DSL

Functoria is a DSL to describe a set of modules and functors, their types and how to apply them in order to produce a complete application.

The main use case is mirage. See the Mirage documentation for details.

Functoria is a DSL to write configuration files for functor-heavy applications. Such configuration files (imaginatively called config.ml) usually contains three parts: one for defining toplevel modules, one for defining configuration kyes and one for defining applications using these modules and keys.

Defining toplevel modules

To define toplevel modules, use the main function. Among its various arguments, it takes the module name and its signature. The type is assembled with the Type combinators, like the @-> operator, which represents a functor arrow.

let main = main "Unikernel.Main" (m @-> job)

This declares that the functor Unikernel.Main takes a module of type m and returns a module of type DSL.job. job has a specific meaning for functoria: it is a module which defines at least a function start, which should have one argument per functor argument and should return unit.

It is up to the user to ensure that the declaration matches the implementation, or be rewarded by a compiler error later on. If the declaration is correct, everything that follows will be.

Defining configuration keys

A configuration key is composed of:

  • name : The name of the value in the program.
  • description : How it should be displayed/serialized.
  • stage : Is the key available only at runtime, at configure time or both?
  • documentation : It is not optional so you should really write it.

Consider a multilingual application: we want to pass the default language as a parameter. We will use a simple string, so we can use the predefined description Key.Arg.string. We want to be able to define it both at configure and run time, so we use the stage Both. This gives us the following code:

let lang_key =
  let doc =
    Key.Arg.info ~doc:"The default language for the application."
      [ "l"; "lang" ]
  in
  Key.create "language" @@ Key.Arg.(opt ~stage:`Both string "en" doc)

Here, we defined both a long option "--lang" and a short one "-l" (the format is similar to the one used by Cmdliner. In the application code, the value is retrieved with Key_gen.language ().

The option is also documented in the "--help" option for both the configure subcommand (at configure time) and ./app.exe (at startup time).

      -l VAL, --lang=VAL (absent=en) The default language for the application.

Defining applications

To register a new application, use register:

let () = register "app" [ main $ impl ]

This function (which should only be called once) takes as argument the name of the application and a list of jobs. The jobs are defined using the Impl DSL; for instance the operator $ is used to apply the functor main (aka Unikernel.Main) to the default console.

Once an application is registered, it can be configured and built using command-line arguments.

Configuration keys we can use be used to switch implementation at configure time. This is done by using the Key DSL, for instance to check whether lang_key is instanciated with a given string:

let lang_is "s" = Key.(pure (( = ) s) $ value lang_key)

Then by using the if_impl combinator to choose between two implementations depending on the value of the key:

let impl = if_impl (is "fi") finnish_impl not_finnish_implementation
module type DSL = module type of DSL

The Functoria DSL allows users to describe how to create portable and flexible applications. It allows to pass application parameters easily using command-line arguments either at configure-time or at runtime.

include DSL

Combinators

type 'a typ = 'a Type.t

The type for values representing module types.

val typ : 'a -> 'a typ

type t is a value representing the module type t.

val (@->) : 'a typ -> 'b typ -> ('a -> 'b) typ

Construct a functor type from a type and an existing functor type. This corresponds to prepending a parameter to the list of functor parameters. For example:

kv_ro @-> ip @-> kv_ro

This describes a functor type that accepts two arguments -- a kv_ro and an ip device -- and returns a kv_ro.

type 'a impl = 'a Impl.t

The type for values representing module implementations.

val ($) : ('a -> 'b) impl -> 'a impl -> 'b impl

m $ a applies the functor m to the module a.

type abstract_impl = Impl.abstract

Same as impl but with hidden type.

val dep : 'a impl -> abstract_impl

dep t is the (build-time) dependency towards t.

Keys

type 'a key = 'a Key.key

The type for configure-time command-line arguments.

type 'a runtime_arg = 'a Runtime_arg.arg

The type for runtime command-line arguments.

val runtime_arg : pos:(string * int * int * int) -> ?name:string -> ?packages:Package.t list -> ('a, Format.formatter, unit, Runtime_arg.t) format4 -> 'a
type abstract_key = Key.t

The type for abstract keys.

type context = Context.t

The type for keys' parsing context. See Key.context.

type 'a value = 'a Key.value

The type for values parsed from the command-line. See Key.value.

val key : 'a key -> Key.t

key k is an untyped representation of k.

val if_impl : bool value -> 'a impl -> 'a impl -> 'a impl

if_impl v impl1 impl2 is impl1 if v is resolved to true and impl2 otherwise.

val match_impl : 'b value -> default:'a impl -> ('b * 'a impl) list -> 'a impl

match_impl v cases ~default chooses the implementation amongst cases by matching the v's value. default is chosen if no value matches.

Package dependencies

For specifying opam package dependencies, the type package is used. It consists of the opam package name, the ocamlfind names, and optional lower and upper bounds. The version constraints are merged with other modules.

type package = Package.t

The type for opam packages.

type scope = Package.scope

Installation scope of a package.

val package : ?scope:scope -> ?build:bool -> ?sublibs:string list -> ?libs:string list -> ?min:string -> ?max:string -> ?pin:string -> ?pin_version:string -> string -> package

Application Builder

Values of type impl are tied to concrete module implementation with the device and main construct. Module implementations of type job can then be registered into an application builder. The builder is in charge if parsing the command-line arguments and of generating code for the final application. See Functoria.Lib for details.

type info = Info.t

The type for build information.

val main : ?pos:(string * int * int * int) -> ?packages:package list -> ?packages_v:package list value -> ?runtime_args:Runtime_arg.t list -> string -> 'a typ -> 'a impl

main name typ is the functor name, having the module type typ. The connect code will call <name>.start.

  • If packages or packages_v is set, then the given packages are installed before compiling the current application.

Devices

type 'a code = 'a Device.code
val code : pos:(string * int * int * int) -> ('a, Format.formatter, unit, 'b code) format4 -> 'a
type 'a device = ('a, abstract_impl) Device.t
val of_device : 'a device -> 'a impl

of_device t is the implementation device t.

val impl : ?packages:package list -> ?packages_v:package list Key.value -> ?install:(Info.t -> Install.t) -> ?install_v:(Info.t -> Install.t Key.value) -> ?keys:Key.t list -> ?runtime_args:Runtime_arg.t list -> ?extra_deps:abstract_impl list -> ?connect:(info -> string -> string list -> 'a code) -> ?dune:(info -> Dune.stanza list) -> ?configure:(info -> unit Action.t) -> ?files:(info -> Fpath.t list) -> string -> 'a typ -> 'a impl

impl ... is of_device @@ Device.v ...

Jobs

type job
module Package : sig ... end

Representation of opam packages.

module Info : sig ... end

Information about the final application.

module Install : sig ... end
module Device : sig ... end

Signature for functoria devices. A device is a module implementation which contains a runtime state which can be set either at configuration time (by the application builder) or at runtime, using command-line arguments.

Useful module implementations

val job : job typ

job is the signature for user's application main module.

val noop : job impl

noop is an implementation of job that holds no state, does nothing and has no dependency.

type argv

The type for command-line arguments, similar to the usual Sys.argv.

val argv : argv typ

argv is a value representing argv module types.

val sys_argv : argv impl

sys_argv is a device providing command-line arguments by using Sys.argv.

val runtime_args : ?runtime_package:package -> ?runtime_modname:string -> argv impl -> job impl

runtime_args a is an implementation of job that holds the parsed command-line arguments. By default runtime_package is "mirage-runtime.functoria" and runtime_modname is "Functoria_runtime".

module Type : sig ... end

Representation of module signatures.

module Impl : sig ... end
module Context : sig ... end

Universal map of keys

module Key : sig ... end

Configuration command-line arguments.

module Runtime_arg : sig ... end

Define runtime command-line arguments.

module Opam : sig ... end
module Lib : sig ... end

Application builder. API for building libraries to link with config.ml

module Tool : sig ... end

Creation of CLI tools to assemble functors.

module Engine : sig ... end

Functoria engine.

module DSL : sig ... end

The Functoria DSL allows users to describe how to create portable and flexible applications. It allows to pass application parameters easily using command-line arguments either at configure-time or at runtime.

module Cli : sig ... end

Command-line handling.

module Action : sig ... end

Wrapper around Bos which provides a "dry run" feature.

module Dune : sig ... end

Dune files.

OCaml

Innovation. Community. Security.