KeyValue
User's Manual - version: 0.2

12. Design: the basics

This section covers some basic aspects of KeyValue's design. The material is kept at the minimum just enough to give the reader all he/she needs to use KeyValue.

All KeyValue classes, functions, templates, etc. belong to namespace ::keyvalue.

12.1. Basic types

Inspired by spreadsheet applications, KeyValue uses five basic types:

The first two types above are C++ built-in types. The other three are library types.

KeyValue introduces value::Nothing to represent empty data.

To maximize portability, KeyValue uses ::std::string and ::boost::posix_time for strings and times, resp. These types are exported to namespace ::keyvalue where they are called string and ptime resp.

The single-value and multi-type container for basic types is value::Variant.

12.2. Values

Values assigned to keys are not always single value::Variant object. They may be containers of value::Variant objects as well. KeyValue provides three such containers:

Actually, bridge and core library developers do not need to care about the containers above. Indeed, they are used exclusively inside KeyValue and, at some point, are converted to more standard types. More precisely, a value::Single object is converted into an appropriate basic type T while a value::Vector object becomes a ::std::vector<T> and a value::Matrix is transformed into a ::std::vector<std::vector<T> > object.

Class value::Value is a single-value and multi-type container for value::Single, value::Vector or value::Matrix objects.

12.2.1. Hierarchy of types and multi-level implicit conversions

Only value::Value objects are returned from KeyValue to front-ends. Hence, a series of conversions must be performed when one wants to return a more basic type. For instance, suppose that a double value x must be returned. In that case the sequence of conversions would be:

return value::Value(value::Single(value::Variant(x)));

Statements like the one above would be needed very often, which is very annoying. Fortunately, KeyValue implements a hierarchy tree of types that allow for multi-level implicitly conversions. Therefore, in the example above, the simpler statement

return x;

would be implicitly converted by the compiler into the one previously shown.

The hierarchy of types constitutes a tree where each node is defined by a specialization of template struct Parent.

12.3. Keys

Initially a key is just a text labeling a value. However, there is more inside a key that just an string object can model. Consider the key Dates in the introductory example again. Its associated value is expected to verify certain conditions:

  • The corresponding value::Value object is made of ptime object(s) rather than, say, double ones.

  • Given the plural in Dates, one can expect more than one ptime and, then value::Value's content might be a value::Vector (of ptimes).

  • Since to each date corresponds a stock price, those dates cannot be in the future.

  • Additionally, one can expect the dates to be in increasing order.

This kind of information is encapsulated by a certain class. In KeyValue terminology, those classes are called real keys and belong to namespace ::keyvalue::key.

The class key::Key is the base of all real keys. More precisely, real keys derive from key::Traits which derives from key::Key.

Actually, key::Traits is a template class depending on a few parameters:

ElementType

Type parameter which defines the type of elements in the output container. It can be bool, double, ptime, string, classes defined by the core library, etc.

ConverterType

This template template parameter[2] defines the class responsible to convert the input value container into a more appropriate type for core library's use. (See Section 12.3.1.)

MapType

This template template parameter[2] tells how each value::Variant object in the input container must be mapped into an ElementType object. (See Section 12.3.2.)

12.3.1. Converter type

Conversions between KeyValue containers value::Single, value::Vector or value::Matrix to more standard types are responsibility of container converter classes.

KeyValue provides three such templates (described below) depending on a parameter ElementType.

key::StdSingle<ElementType>

Converts from value::Single into ElementType.

key::StdVector<ElementType>

Converts from value::Vector to ::boost::shared_ptr<std::vector<ElementType> >.

key::StdMatrix<ElementType>

Converts from value::Matrix to ::boost::shared_ptr<std::vector<std::vector<ElementType> > >.

If the core library uses non-standard containers, then bridge developers have two choices. They can either use the converters above as a first step and then convert again to desired types; or they can implement new container converters that produce the desired types directly from KeyValue containers. The second option is clearly more efficient.

To implement new container converters, reading the reference documentation of the three container converters above it strongly advised. Moreover, their implementations can serve as samples for implementing new ones.

12.3.2. Map type

Similarly to lexical conversions but depending on the key, sometimes, each element of the input container must be mapped to a special value. For instance, for a key Month, it may be convenient to map strings "Jan", "Fev", ..., "Dec" into numbers 1, 2, ..., 12. This is an example of key::FlagMap.

Mappings are performed by classes which implement a method to convert from a value::Variant objects into other types of objects. Actually, they are template classes depending on a parameter named OutputType which defines (but not necessarily matches) the real output type. The real output type might be recovered through the member type OutputType_.

The map template classes are the following:

key::NoMap<OutputType>

Through this map, a value::Variant object holding a value x is mapped into an object of type OutputType which has the same lexical value as x. Only front-end enabled lexical conversions are considered. For instance, a value::Variant object holding either the double 10.1 or the string "10.1" is mapped into the double (OutputType in this case) 10.1.

key::FlagMap<OutputType>

Some string values are accepted others not. The accepted ones are mapped into particular values of type OutputType. In the example of key Month above, OutputType can be double, unsigned int or an enum type.

key::PartialMap<OutputType>

Half way between key::NoMap and key::FlagMap. First, like key::NoMap, considering front-end enabled lexical conversions, it tries to map a value::Variant value into an object of type OutputType which has the same lexical value as x. If it fails, then, like key::FlagMap, it tries to map a string into a corresponding value of type OutputType. For instance, the value for NumberOfEdges (of a regular polygon) must be an unsigned int greater than 2. For some special values (not all) there correspond a polygon name (e.g. "Triangle" for 3 or "Square" for 4). There is no special name for a regular polygon with 1111 edges.

key::ObjectMap<OutputType>

This is a map where a string identifier is mapped into a ::boost::shared_ptr<OutputType> pointing to an object of type OutputType. Notice that this is the only map where OutputType and OutputType_ differ.

12.3.3. Generic keys

There are some basic properties shared by several types of keys. For instance, Price, Weight, Size, etc., accept only positive numeric values. Although one can implement one class for each of them, this would imply extensive code duplication. Therefore, KeyValue implements a few generic keys which can be used for those having basic properties. Only very specific and application dependent keys need to be implemented as new real keys.

All generic keys set their label at construction time. They are:

key::Single<ElementType>

Key for a single object of type ElementType. No constraints on the value are set.

Example: A key labeled Number which accepts any double value is defined by

key::Single<double> key("Number");
key::Vector<ElementType>

Key for a vector of objects of type ElementType with no constraints on them. A restriction on the size of the vector might be set on construction.

Example: A key labeled Names which expects a vector of 5 strings is defined by

key::Vector<string> key("Names", 5);
key::Matrix<ElementType>

Key for a matrix of objects of type ElementType with no constraints. A restriction on the matrix dimension can be set at construction.

Example: A key labeled Transformation which accepts a 2x3 matrix is defined by

key::Matrix<double> key("Transformation", 2, 3);
key::Positive

Key for a positive number.

Example: A key labeled Price is defined by

key::Positive key("Price");
key::StrictlyPositive

Key for a strictly positive number.

Example: If the key in the previous example could not accept the value 0, then it would be defined by

key::StrictlyPositive key("Price");
key::Bounded<ElementType, Bound1, Bound2>

Key for a single bounded value of type ElementType. Template template parameters[2] Bound1 and Bound2 define the bound types and can be either key::Greater, key::Geq (greater than or equal to), key::Less or key::Leq (less than or equal to).

Example: A key labeled Probability accepting any double value from and including 0 up to and including 1 is defined by

key::Bounded<double, key::Geq, key::Leq> key("Probability", 0.0, 1.0);
key::MonotoneBoundedVector<ElementType, Monotone, Bound1, Bound2>

Key for vectors whose elements are monotonic and/or bounded. Template template parameter[2] Monotone defines the type of monotonicity and can be either key::NonMonotone, key::Increasing, key::StrictlyIncreasing, key::Decreasing, key::StrictlyDecreasing. Bound1 and Bound2 are as in key::Bounded. Additionally, a constraint on the vector size can be set on construction.

Example: A key labeled Probabilities accepting 10 strictly increasing numbers from and excluding 0 up to and including 1 is defined by

key::Bounded<double, key::StrictlyIncreasing, key::Greater, key::Leq>
  key("Probabilities", 0.0, 1.0, 10);

12.4. DataSet

Key-value pairs are stored in DataSets. This class implements methods getValue() and find() to retrieve values assigned to keys. Both methods receive a real key and processes all the information about the expected value encapsulated by the key. For instance, suppose the variable today holds the current date and consider a key BirthDates which corresponds to a vector of increasing dates, supposedly, in the past or today.

An appropriate real key is then:

key::MonotoneBoundedVector<ptime, key::Increasing, key::Leq>
  births("BrithDates", today);

Therefore, if key BirthDates belongs to a DataSet data, the result of

data.getValue(births);

is a ::boost::shared_ptr<const ::std::vector<ptime> > such that the elements of the pointed vector are in increasing order and before (less than or equal to) today. The caller does not need to check that.

Since the type returned by getValue() depends on the real key it receives, this method is a template function. The same is true for find().

The difference between getValue() and find() concerns what happens when the key is not resolved. The former method throws an exception to indicate the failure while the latter returns a null pointer. In practice, getValue() is used for compulsory keys and find() for optional ones. A typical use of find() follows:

bool foo(false);
if (bool* ptr = data.find(key::Single<bool>("Foo")))
  foo = *ptr;

In the code above foo is false unless key Foo is found in data, in which case, foo gets the given value.

12.5. Processors

All builders and calculators derive from class Processor. This class declares two pure virtual methods: getName() and getResult(). The former method returns the name under which the processor is recognized by key Processor. The second gets the result of processing a DataSet.

Actually, builders and calculators are specializations of template classes Builder and Calculator, resp. They depend on a parameter type named either OutputType (for Builder) or Tag (for Calculator). The primary role of these parameters is to distinguish between different specializations. For a Builder, OutputType also defines the type of object built.

Although a Builder specialization is uniquely identified by OutputType, it also needs to be assigned to a Tag for a unified registration process of Builders and Calculators into the global ProcessorMngr. (See Section 13 for more information on this registration process.)

Builder and Calculator specializations may implement different features which define their declaration and implementation. Therefore, different specializations of Builder (or of Calculator) might derive from different base classes and implement different methods. Rather than declare the specializations from scratch, providing all its base classes and declaring all its methods, the helper files keyvalue/mngt/DeclareBuilder.h and keyvalue/mngt/DeclareCalculator.h should be used. (See Builder and Calculator for details).

Builder and Calculator specializations might be Commands as explained below.

12.5.1. Commands

A Processor able to process an empty DataSet might be declared a Command.

For instance, if the Processor is Builder<ObjectType>, for some ObjectType, then Builder<ObjectType>::getObject(const DataSet& data) might do its job ignoring data (which is the case of ListOfDataSets and NumberOfDataSets). Another way is when the method does look up values in an empty data but, failing to find any, can still do its job considering default values for the searched keys (with or without intervention of Default DataSet (see Section 9).

In either cases above, Builder<ObjectType> might be declared a Command by publicly deriving from this class. Similar arguments hold for a Calculator<Tag>.

When a Processor is a Command some front-ends might take advantage of this fact and provide to their users some shortcut or menu entry to call the Processor without asking for additional user's input.

12.5.2. Building from a single value

In general, the information that processors need to perform their duties is so rich that must be stored in a DataSet. Nevertheless, in some particular cases, a single value::Variant might be enough. For instance, consider a builder that creates a curve given a few points on it. Normally, this processor needs the set of points together with interpolator and extrapolator methods. In this general case, a DataSet is necessary to hold all this information. However, when the curve is known to be constant, then a single number - the constant - is enough. Rather than creating a DataSet to store a single number, it would be more convenient if the processor could accept just this value (or more generally, a value::Variant). This is, indeed, the case.

Any Builder specialization which can build from a value::Variant object must derive from template class BuilderFromVariant. Additionally, it also must derive from at least one instantiation of template class BuilderFrom.

12.6. Exceptions and Messages

The abstract class Message defines the interface for all types of messages. MessageImpl is a template class which implements Message's pure virtual methods. There are six different specializations of MessageImpl with corresponding typedefs:

They define operator&() to append formated data to themselves. A typical use follows:

Info info(1);  // Create a level-1 Info message.
size_t i;
std::vector<double> x;
//...
info & "x[" & i & "] = " & x[i] & '\n';

Similarly, exception::Exception is an abstract class whose pure virtual methods are implemented by template class exception::ExceptionImpl. This template class has a member which is an instantiation of MessageImpl. The exact instantiation is provided as a template parameter of exception::ExceptionImpl. There are two specializations of exception::ExceptionImpl with corresponding typedefs:

RuntimeError indicates errors that can be detected only at runtime depending on user data. LogicError indicates errors that should be detected at development time. In other terms, a LogicError means a bug and is thrown when a program invariant fails. It is mainly used through macro KV_ASSERT as in

KV_ASSERT(i < getSize(), "Out of bound!");

To keep compatibility with exception handlers catching standard exceptions, RuntimeError derives from ::std::runtime_error while LogicError derives from ::std::logic_error.

Method exception::ExceptionImpl::operator&() provides the same functionality of MessageImpl::operator&(). Example:

if (price <= 0.0)
  throw RuntimeError() & "Invalid price. Expecting a positive number. Got " &
    price;

Other more specific exception classes are implemented to indicate errors that need special treatment. They all derive from either RuntimeError or LogicError.



[2] Template template parameters belong to the less known features of C++ and then deserve a quick note here. Most template parameters are types. Nevertheless, sometimes a template parameter can be a template, in which case it is referred as a template template parameter. For instance, a template Foo depending on only one template template parameter might be instantiated with Foo<std::vector> but not with Foo<std::vector<int> >. Recall that std::vector is a template class while std::vector<int> is a class.

Valid HTML 4.01 TransitionalValid CSS!