Properties (2)
The Hidden State of Hidden Lines
Last time I introduced you to SFont
, a simple yet useful representation of partially defined ("mixed") fonts:
In our teminology SFont
is called a "property", and its members m_ostrName
, m_onSize
and m_obBold
are called "aspects" of the property. We discussed how SFont
can be seen as representing a set of fonts, or a subspace in the space of all possible fonts, and leveraged those insights to define some useful methods and functions.
The whole point of that last blog article was to provide you with some concepts and notions that would help you design your own properties, not just fonts. In our software, we have a lot of properties that follow the same pattern: fonts, colors, fills, lines, bullets, markers (used in scatter charts and line charts to mark data points), and even precisions (formatting of decimal numbers). Today we will find out how line formats are different from fonts, and how we can amend our simple data structure to make it fit for line formats. Let's start with a straight-forward definition of SLineFormat
, based on PowerPoint's LineFormat API:
Seems simple enough, but what's the meaning of the dash style of an invisible line? How is a "visible" line with weight 0 (zero) different from an "invisible" line? Meet the concept of hidden state: We define hidden state as state (information) that is represented and maintained in a data structure, but is not discernible by the user. Everybody who designs data structures frequently meets hidden state, whether they like it or not. In many cases it is tempting to ignore hidden state in favor of some superficial simplicity and apparent symmetrical beauty, or simply because the developer failed to recognize what later turns out to be hidden state.
Hidden state is not necessarily a bad thing: Consider typing text into a PowerPoint textbox, in some deliberately chosen font. When for some reason you decide to re-write your text, a likely first step is to delete the existing text and return to an empty textbox. An empty textbox does not have any visual representation of font (leaving aside the size of the blinking cursor) and yet you expect that when you start typing, your text will be using the same font that was used before. That behavior is useful, and implementing it requires a deliberate usage of hidden state.
In most scenarios though, hidden state is more confusing than helpful. Typically, hidden state surfaces in the form of remnants of a data model's history. In our product domain, if we are not careful about hidden state, two charts may look identical "pixel-wise", but may exhibit very different behavior when the user, e.g., changes the underlying data. This is particularly irritating if the user received those charts from a third party and has no way of knowing how they were created.
Instances of "useless hidden state" are often referred to as "redundancy", and a general rule of thumb for the design of data structures is that redundancy is bad and should be avoided. Did we overlook any hidden state in SFont
? No we didn't: All aspects of our definition of the SFont
property are independent from each other. Each aspect is meaningful in its own right, regardless of the values of the other aspects. Borrowing a notion from vector spaces, we say that name, size and boldness of a font are orthogonal to each other.
Getting back to SLineFormat
, we find that there are multiple representations of an invisible line. Given that all invisible lines look the same to the user, we can safely say that this is an instance of hidden state. At this point we need to decide if this particular hidden state is useful or harmful. I can immediately name some common use cases where an ambiguous representation of an invisible line is harmful:
- When displaying a list of available line formats in the user interface, we want to show at most one instance of an invisible line format in the list. An invisible line format must compare "equal" with other invisible line formats for this purpose.
- When displaying the current line format of a multi-selection of lines, none of which are visible, the user interface should reflect "invisible" and not "mixed", see method
SFont::Union(SFont)
. - When determining whether the application of one line format on top of another line format would have any effect, we should not come to the conclusion that applying an invisible line format to another invisible line format results in a meaningful change, see method
SFont::IsSupersetOf(SFont)
.
At the same time I find it hard to come up with any use case that would justify the maintenance of hidden state in an invisible line format. Thus, for the sake of this article, let's say that SLineFormat
should not contain any hidden state.
How can we achieve that? For a start, there is a redundancy in members m_obVisible
and m_onWeight
. Either of them can indicate an invisible line format, so ideally only one of them should be needed. Obviously, m_onWeight
is indispensible to represent a visible line format, but m_obVisible
does not contribute any information that isn't also available from m_onWeight
, thus m_obVisible
should be dropped:
Microsoft's MsoLineDashStyle
and MsoLineStyle
enums don't have values for "invisible" or "no line", but there are still redundant representations of an invisible line. How is that? Well, if a line format has m_onWeight==0
, i.e., it represents an invisible line, m_omsolinedashstyle
and m_omsolinestyle
may still have values. Thus there are a lot of different combinations of values in the members of SLineFormat
, that can all represent "no line".
How can we get those meaningless combinations of values under control? There are at least two possible ways:
- We can ignore whatever values there are in
m_omsolinedashstyle
andm_omsolinestyle
whenever we encounterm_onWeight==0
(see listing 4), - or we can ensure that whenever
m_onWeight==0
, the other members always have one and the same unique value (see listing 5).
While ignoring the values of irrelevant aspects sounds simple and most canonical in theory, in practice it turns out that ensuring specific values for meaningless aspects allows for cleaner, simpler, more canonical code. Clean, simple, canonical code means fewer bugs to begin with, and better maintainability, therefore we mostly use the latter approach in our software. In particular, due to the way our properties are designed, each aspect conveniently has an "undefined" (std::nullopt
) state, anyway. Using the "undefined" state for this purpose saves us from dealing with arbitrary magic values (which would also work in principle, of course).
With this basic understanding of dependent (non-orthogonal) property aspects, let's look at the implementations of some other key methods and functions for SLineFormat
:
With (6) we can now use operator<<(...)
to write chained expressions, just as with SFont
, which are simple, elegant and, as we will see below, wrong (quote attributed to H. L. Mencken). For the sake of an example, let's look at our product domain again: We have a global default line format for visible lines, and we have a hierarchy of partially defined default line formats for specific purposes. How do we compose the default for the outline of a highlighted segment in a bar chart?
This expression worked great for SFont
, what's wrong with using it for SLineFormat
? Let's assume we start out with some sensible global default line format. Some modern styles for charts use colored areas without outlines, so lineDefaultSegment
may be set to the unambiguous invisible outline. Highlighting a segment may justify a particularly thick outline (maybe even a red one, but we don't support color in our toy example), and it shall use whatever MsoLineDashStyle
and MsoLineStyle
is defined in the global default. Thus, our defaults may look like this:
If we feed the values from listing (8) into expression (7), the result is a partially defined line format: {/monWeight/ 12, /momsolinedashstyle/ std::nullopt, /m_omsolinestyle/ std::nullopt}
. Obviously, this is not useful: In order to display a line around a highlighted segment, knowing the thickness is not enough. We need some well-defined MsoLineDashStyle
and MsoLineStyle
, too. Where exactly did msoLineSolid
and msoLineSingle
of lineDefaultGlobal
get lost? The answer is: Associativity.
Operator precedence rules of C++ state that <<
is evaluated from left to right. If we hit one invisible line format in our chain of <<
operations, then according to listing (6) we clean out all hidden state. When we then hit another line format, that is partially defined visible, we cannot recover the missing aspects from the left-most operand. This can easily be fixed: We need to evaluate the chain from right to left:
That's it for today! If there are only two things that you take away from this article, it should be that whenever you design a data structure, be aware of hidden state. And whenever you encounter hidden state, make a conscious decision how you want to deal with it, because that decision will affect the complexity of your code and the pitfalls you'll encounter when using your data structure. As always, if you have any feedback, don't hesitate to get in touch!
Do you have feedback? Send us a message at devblog@think-cell.com !
Sign up for blog updates
Don't miss out on new posts! Sign up to receive a notification whenever we publish a new article.
Just submit your email address below. Be assured that we will not forward your email address to any third party.
Please refer to our privacy policy on how we protect your personal data.