package functoria

  1. Overview
  2. Docs
A DSL to organize functor applications

Install

Dune Dependency

Authors

Maintainers

Sources

functoria-2.1.0.tbz
sha256=83fa056e52fe1fdc996d68ad2ce50f490d44e4b9cdd67e8c88a05753d99e70ec
md5=4541047f19a2842df17b9ffb5badbb3e

Description

Build Status docs

What is this for?

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 repository for details.

How to write a configuration file?

There are numerous examples of configuration files in mirage-skeleton. Most of them should be fairly general and understandable, even outside the context of mirage. We can distinguish two parts in a config.ml: Defining new modules and using them.

In order to define a new module, we use the foreign function. Among its various arguments, it takes a module name and a type. The type is assembled with the DSL's combinators and the @-> operator, which symbols a functor arrow.

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

Here, we declare the functor Unikernel.Main that takes a module that should be a console and returns a module that is a job. It is up to the user to ensure that the declaration matches the implementation (or be punished by a compiler error later on). If the declaration is correct, everything that follows will be.

We can now use this declaration:

let () = register "console" [main $ default_console]

Here, we register a new application with the register function. This function should only be called once and takes as argument the name of the application and a list of jobs. We use the $ operator to apply the functor main (aka Unikernel.Main) to the default console.

Now that everything is ready, you can use the configure subcommand!

What is a job?

A job is a module containing a function start. This function will receive one argument per functor argument and one per dependency, in this order. foreign assumes the function start returns unit.

Defining new keys

A 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.Desc.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.Doc.create
      ~doc:"The default language for the application." [ "l" ; "lang" ]
  in
  Key.create ~doc ~stage:`Both ~default:"en" "language" Key.Desc.string

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 ./my_application (at startup time).

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

Using switching keys

We can do much more with keys: we can use them to switch implementation at configure time. Imagine we want to completely change some implementation based on the language. Finns are special snowflakes, they deserve their special application!

First, we have to compute a boolean value from lang:

let is_fi = Key.(pure ((=) "fi") $ value lang_key)

We can use the if_impl combinator to choose between two implementations depending on the value of the key:

let dynamic_storage =
  if_impl is_fi
    finnish_implementation
    not_finnish_implementation

This distinction will be visible using the describe subcommand and a dot diagram is available with the --dot option!

Internals

Phases

Configuration is separated into phases:

  1. Specialized DSL keys The specialized DSL's keys (along with functoria's keys) are resolved.
  2. Compilation and dynlink of the config file.
  3. Registering. When the register function is called, the list of jobs is recorded and immediately transformed into a graph.
  4. Switching keys and tree evaluation. The switching keys are the keys inside the [If]. Those keys are resolved and the graph is simplified. At this point, the actual modules used are fully known. Note: for the describe command, Only partial evaluation is done, which means decision nodes are resolved only if the value was given on the command line, disregarding default values.
  5. Full Key resolution. Once the actual modules are known, we can resolve all the keys and figure out libraries and packages.
  6. Dependency handling, configuration and code emission.

Phases 1. to 4. are also applied for the clean command.

Tags

org:mirage

Published: 04 Jul 2017

README

Functoria - A DSL to organize functor applications

What is this for?

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 repository for details.

How to write a configuration file?

There are numerous examples of configuration files in mirage-skeleton. Most of them should be fairly general and understandable, even outside the context of mirage. We can distinguish two parts in a config.ml: Defining new modules and using them.

In order to define a new module, we use the foreign function. Among its various arguments, it takes a module name and a type. The type is assembled with the DSL's combinators and the @-> operator, which symbols a functor arrow.

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

Here, we declare the functor Unikernel.Main that takes a module that should be a console and returns a module that is a job. It is up to the user to ensure that the declaration matches the implementation (or be punished by a compiler error later on). If the declaration is correct, everything that follows will be.

We can now use this declaration:

let () = register "console" [main $ default_console]

Here, we register a new application with the register function. This function should only be called once and takes as argument the name of the application and a list of jobs. We use the $ operator to apply the functor main (aka Unikernel.Main) to the default console.

Now that everything is ready, you can use the configure subcommand!

What is a job?

A job is a module containing a function start. This function will receive one argument per functor argument and one per dependency, in this order. foreign assumes the function start returns unit.

Defining new keys

A 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.Desc.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.Doc.create
      ~doc:"The default language for the application." [ "l" ; "lang" ]
  in
  Key.create ~doc ~stage:`Both ~default:"en" "language" Key.Desc.string

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 ./my_application (at startup time).

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

Using switching keys

We can do much more with keys: we can use them to switch implementation at configure time. Imagine we want to completely change some implementation based on the language. Finns are special snowflakes, they deserve their special application!

First, we have to compute a boolean value from lang:

let is_fi = Key.(pure ((=) "fi") $ value lang_key)

We can use the if_impl combinator to choose between two implementations depending on the value of the key:

let dynamic_storage =
  if_impl is_fi
    finnish_implementation
    not_finnish_implementation

This distinction will be visible using the describe subcommand and a dot diagram is available with the --dot option!

Internals

Phases

Configuration is separated into phases:

  1. Specialized DSL keys The specialized DSL's keys (along with functoria's keys) are resolved.

  2. Compilation and dynlink of the config file.

  3. Registering. When the register function is called, the list of jobs is recorded and immediately transformed into a graph.

  4. Switching keys and tree evaluation. The switching keys are the keys inside the [If]. Those keys are resolved and the graph is simplified. At this point, the actual modules used are fully known. Note: for the describe command, Only partial evaluation is done, which means decision nodes are resolved only if the value was given on the command line, disregarding default values.

  5. Full Key resolution. Once the actual modules are known, we can resolve all the keys and figure out libraries and packages.

  6. Dependency handling, configuration and code emission.

Phases 1. to 4. are also applied for the clean command.

Dependencies (11)

  1. fpath
  2. bos
  3. logs
  4. ocamlgraph
  5. fmt
  6. astring
  7. rresult
  8. cmdliner >= "0.9.8" & < "1.1.0"
  9. base-unix
  10. jbuilder >= "1.0+beta10"
  11. ocaml >= "4.03" & < "4.07.0"

Dev Dependencies (1)

  1. ounit with-test & >= "2.0.0"

Used by (1)

  1. mirage >= "3.0.0" & < "3.0.5"

Conflicts

None

OCaml

Innovation. Community. Security.