Lukas Lazarek <lukas dot lazarek at eecs dot northwestern dot edu>
This library implements a simple system for software configuration. The idea is that the system implementor can create configurable features which have multiple implementations that users can select from. Users can then create a config that selects an implementation for each configurable feature. The system can install such a config to configure the features.
This library supports that workflow with the following design.
an interface of values related to the feature
one or more implementations, identified by module paths.
With the configurable feature set definition in hand, the system implementor can require it to access each feature’s interface through parameters holding their currently configured values.
A config defines a mapping from each feature’s interface to concrete values by selecting an implementation for each feature. The system sets the current configuration values by installing a config with install-configuration!. Configs are written in the #lang configurable/config DSL.
This example illustrates implementing a tool that searches text by lines (think grep). The tool should allow users to select a search algorithm from three choices: literal matching, regexp, and fuzzy search (at least initially - we will see how the this library makes it easy to add more down the road).
Our tool’s implementation might initially look like this.
;;tool.rkt#langracket;;swapoutwithregexp.rktorfuzzy.rkt#:args[query.files-to-search](search!queryfiles-to-search))
With the different search styles implemented in their own files.
;;literal.rkt#langracket;;todo...
;;regexp.rkt#langracket;;todo...
;;fuzzy.rkt#langracket;;todo...
To let users select a search style via a config file instead of modifying the tool, we’ll write a configurable feature set definition describing our configurable feature (the search style) and our known implementations. Then we’ll write a config that selects a style, which users can edit instead of editing the tool itself.
The feature set definition looks like this.
;;configurables.rkt#langconfigurable/definition#:provides[search!]#:module"literal.rkt")#:module"regexp.rkt")#:module"fuzzy.rkt"))
And a config file looks like this.
;;search-config.rkt#langconfigurable/config"configurables.rkt";;`fuzzy`hereisboundbythedefinitioninconfigurables.rkt
Finally we refactor our tool to look like this. Each change is annotated with a comment.
;;tool.rkt#langracket;;obtaintheconfigpath...#:once-each[("--config""-c")path"searchstyleconfigurationtouse";;...andinstallit(install-configuration!path)]#:args[query.files-to-search];;accesstheconfiguredsearchfunctionwiththeparameter`configured:search!`;;whichiscreatedby`define-configurable!`((configured:search!)queryfiles-to-search))
Under the hood, what’s happening is that install-configuration! sets the value of the parameter configured:search!.
Of course, this example is a bit contrived because a single command-line switch would suffice to configure the search style. However, that approach quickly grows unwieldy when there are several features to configure, and especially so if some implementations themselves may be parameterized.
This library offers a natural solution for the second challenge of parameterized features as well, with implementation parameters. Implementation parameters are essentially arguments that can be specified in a config file to configure an implementation.
Let’s see how that works by adding a new feature to our search tool. We’ll support abbreviations in literal search queries, so that doing a literal search for @myemail instead searches for joe-schmoe9000@gmail.com. A table of abbreviation definitions in the user’s config file will define the set of these to use.
First, let’s update literal.rkt to support these abbrevs.
;;literal.rkt#langracketcurrent-abbrevs);;todo...
Next, let’s update the configurable feature set definition.
;;configurables.rkt#langconfigurable/definition#:provides[search!]#:module"literal.rkt"#:parameters[current-abbrevs])#:module"regexp.rkt")#:module"fuzzy.rkt"))
And finally we can use it in the config.
;;search-config.rkt#langconfigurable/config"configurables.rkt"
The config would not need to change at all for other search styles, on the other hand. For instance, the same fuzzy-searching config we had before is still valid now.
configurable feature set definitions are written in this language, and consist of a sequence of define-configurable forms at the top level.
The resulting module provides the names of all the defined configurable features, the features configured: parameters, and the operations on configs described in Config operations.
syntax
#:provides[id...]implementation-definition...)
Each implementation-definition must be an define-implementation form.
This form creates and provides a parameter for each id named configured:id which will hold the currently configured implementation’s version of id.
syntax
#:modulerelative-module-pathmaybe-parameters)maybe-parameters =| #:parameters[parameter-id...]
Each parameter-id specified defines an implementation parameter.
configs are written in this language, which is parameterized by the configurable feature set definition whose relative path is provided after the #lang (see the example configs above). Configs consist of a sequence of configure! or configure-all! forms at the top level.
syntax
( configure! feature-idimplementation-idparameter-value...)
syntax
( configure-all! [feature-idimplementation-idparameter-value...]...)
These operations are also provided by every configurable feature set definition, which is the more typical way to obtain them.
procedure
( install-configuration!path)→any
path:path-string?
procedure
( call-with-configurationpaththunk)→any
path:path-string?
procedure
( current-configuration-path)→(or/c path-string? #f)