KeyValue
User's Manual - version: 0.1

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 given by a single value::Variant. They may be containers of value::Variants. KeyValue provides three such containers:.

Actually, bridge and core library developers do not need to care neither 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 is converted into an appropriate basic type a T while a value::Vector becomes a ::std::vector<T> and a value::Matrix is transformed into ::std::vector<std::vector<T> > .

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

12.2.1. Hierarchy of types and multi-level implicit conversions

Only value::Values 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 variable 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 string labeling a value. However, there is more inside a key that just a string can model. Consider the key Dates in the introductory example again. The value associated to it is expected to verify certain conditions:

  • The corresponding value::Value is made of ptimes.

  • One can expect more than one ptime and, then value::Value's container might be a value::Vector (of ptimes).

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

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

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

The class is key::Key is the base for 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 template 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[3] 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[3] tells how each value::Variant in the input container must be mapped into an ElementType. (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, the reading of reference documentation of the three container converters above it strongly advisable. Moreover, their implementations can serve as samples.

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 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 value into other type. 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 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 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 strings are accepted. Each of them is mapped into a particular value of type OutputType. In the example above OutputType is double.

key::PartialMap<OutputType>

A mix between key::NoMap and key::FlagMap. First, like key::NoMap, considering front-end enabled lexical conversions, it tries to map a value::Variant 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_ do not match.

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 number for values. Although one can implement one class for each of them, this would imply in extensive code duplication. Therefore, KeyValue implements a few generic keys which can be used for keys having basic properties. Only very specific and application dependent keys need to be implied as new real keys.

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

key::Single<ElementType>

Key for a single value 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 values of type ElementType with no constraints on them. A restriction on the size of the vector might be set at 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 values 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 in 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 values of type ElementType. Template template parameters[3] 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[3] 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 at 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 also find() to retrieve values assigned to keys. Both methods receive a real key and processes all the information about the expected value encapsulated in it. For instance, suppose that the variable today holds the current date and consider a key BirthDates which corresponds to a vector of increasing dates, supposedly, in the past.

An appropriate real key is then:

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

Therefore, if key BirthDates is in a DataSet named data, the result of

data.getValue(births);

is a ::boost::shared_ptr< const ::std::vector<ptime> > such that the elements of the vector pointed by this pointer 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 keys. A typical use of find() is as 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 which case, foo gets the given value.

12.5. Processors

All builders and calculators derive from protocol class Processor. This class declares three pure virtual methods: getName() and two overloads of getResult(). The former method returns the name under which the processor is recognized by key Processor. The first overload of getResult() takes a DataSet parameter while the second one takes a value::Variant. The difference between them is explained below.

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.

Actually, builders and calculators are specializations of template classes Builder and Calculator, resp. They depend on a parameter type named either OutputType (for a builder) or Tag (for a calculator). The primary role of this parameter is to distinguish between different specializations. Additionally, in the case of a builder, it also defines the type of object constructed.

The header files of Builder and Calculator implement the two overloads of Processor::getResult(). The first overload forwards the call to either Builder<ObjectType>::getObject() or Calculator<Tag>::getValue(). These methods must be implemented by bridge developers whenever the template classes are used.

As explained earlier, processing a value::Variant does not always make sense. Therefore, the second overload of Processor::getResult() throws an exception. When this processing does make sense, then template classes XBuilder and XCalculator which extend their base classes Builder and Calculator, resp., must be used. Their header files reimplement the second overload of Processor::getResult() forwarding the call to XBuilder<ObjectType>::getObject() and XCalculator<Tag>::getValue(). As before, these two methods must be implemented by bridge developers whenever the extended template classes are used.

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 in template class exception::ExceptionImpl. This template class has a member which is an instance of MessageImpl. The exact type of this instance is provided as a template parameter of exception::Exception. There are two specializations of exception::Exception 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.



[3] 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 while std::vector<int> is a type.

Valid HTML 4.01 TransitionalValid CSS!