Generics in the FPL
Author: Arne Bergmann | Developer at FRIENDSHIP SYSTEMS AG
Remember Effective Feature Programming Language (FPL) document ? If not please go read it first. Otherwise, I am afraid the following text might completely discourage you to do anything with Features.
Ok, it's time to talk about programming again. Let’s talk about control structures and types. Especially the latter is the main topic of this text.
In Effective FPL the focus was on control structures and how to use them efficiently with the types CAESES provides. This part of the series (well, it's hard to talk about a series, yet, but I have the feeling that there may be more) is more focused on types and how to use the operations they offer. I would not go so far, though, as to call this document a type reference - it's actually far from that - but there are some new types that have been added to CAESES and I want to introduce them to you to give you head-start on how to use them.
So, in the glimpse-into-the-future-part of Effective FPL I said that the most important addition (IMHO) to the FPL would be type-safe containers. Well, guess what, they've arrived! I just finished adding the concept of generic-types to the FPL which made the implementation of type-safe containers a breeze. "WTF are generic-types" you say? Well, in that case, fasten your seat belts. This is going to be a bumpy ride (yes, it's very early to pass out cookies, but a cookie to those that got that movie reference, and we are not going to have to fight any zombies, I promise!)
Introduction
I will skip the part where I introduce Features and try to convince you that Features are super important in order to use CAESES efficiently. I'll just assume you already know that. And if you do know everything about Features and maybe have even read Effective FPL and you still think otherwise, it's good for you (and a nice sign for us, since it means you are happy with CAESES just as it is; no customization needed). But, to be honest, if that is the case, this text is not for you.
Also, even for those of you that are already convinced of the importance and beauty of Features and maybe even are experienced Feature-artists, let me add a disclaimer: some of the stuff that I am covering here will not be for the faint hearted. I will try to go easy on you, though. Also, keep in mind that in order to use Features effectively, you do not need to completely understand everything I am going to tell you. You can just keep on going with what you are currently doing, especially if you follow my advice in Effective FPL ; all of the established techniques and best-practices stay absolutely valid. We are now entering new territory.
This disclaimer doesn't mean that we've added a lot of stuff for programming experts only or new stuff that is not worth learning. It just means that some of the concepts might take a little time to wrap your head around. Let me put this straight:
- we've tried to keep it as simple as possible,
- once you've succeeded to wrap your head around the concept, it can be extremely powerful, and
- (I hope that) once you've read this text you will be able to use all of that new stuff efficiently.
Ok, now that I got that off my chest, let's dive right in...
Generic-Types
We've added a new concept to the FPL: generic types. Data types offer operations and algorithms
that operate on the data that the data type is made up of, i.e. objects of other (or even the same)
data types. Now, for a certain range of types, the operations they offer is the same independently of
the actual data they operate on. Probably the most basic example for such a type is a list. Lists store
other objects (possibly other lists) and allow to access their content (of course, they could offer
other, more advanced functions like sorting, for example, but let's leave it at this simple case).
This means that the interface of a list basically contains of two operations: "store" and "remove". Those two operations will always operate in the same way, no matter what type of objects that list contains.
You can think of a list as a storage room. You can put stuff into it (store) and you can take stuff out of it (remove). However, it does not matter what kind of stuff you put into the storage room as long as the physical space is big enough for the items you put into it.
Ok, let's leave the real world again; we are talking about programming here. For programming another operation needs to be added to the interface of our storage room, because for digital data there is a difference between getting something out of our storage and removing it from the storage, since just accessing the data does not necessarily remove it from the list. For the storage room example this would be equivalent to going into the room and use the item inside of the room without removing it. So the three basic operations that a list needs to offer are actually “store”, “remove”, and “get”. This is the same for every list, no matter what type of objects it contains.
However, the actual signature of those operations changes depending on the contained type.
A list that stores double values needs to return objects of the double type from the get-operation, while a list that stores points needs to return point objects. So, basically you'd have these two signatures:
FDouble FList.get(FUnsigned index)
FPoint FList.get(FUnsigned index)
Internally, both do exactly the same, though: "access the data that is stored in the list at the requested index and return it to me".
Let's first take a look at the current state of CAESES. It already offers a list type that can store any
type of object. That is the FObjectList type. That type already fulfills the interface mentioned above,
but it does so by making use of the inheritance hierarchy in CAESES. It utilizes the type that is
common to all objects: FObject. This works, but it comes with some draw-backs. First of all, it
requires a lot of typing when accessing elements of a list, because the returned values need to be
casted to the desired type and (for good manners) a test is needed whether the cast succeeded. The
success check isn't all that easy, though, since basictypes cannot be checked for NULL (which would
normally be the return value of a cast if it fails). Take a look at the following code:
objectlist list()
line l() // create a line
list.add(l) // add line to the objectlist
FLine lref(list.at( 0 ).castTo(FLine)) // cast the line in the list to FLine and store result in lref
FPoint pref(list.at( 0 ).castTo(FPoint)) // cast the line in the list to FPoint and store result in pref
FDouble d(list.at( 0 ).castTo(FDouble)) // cast the line in the list to FDouble and store result in d
Ok, this looks fine, doesn’t it? The only problem is that there is a possible bug in here! Let’s take a
look at the values of lref, pref, and d in the debugger:
As we can see, lref holds a reference to our original line (see Item 5 in Effective FPL ), pref has the value NULL (because an FLine cannot be casted to FPoint ), but d is a valid FDouble with the value 0! So,
there is no way to find out whether the objectlist actually contains a double at any given index; if it
doesn’t it will just create a double with the value 0. Well, there is a way, but it’s a very nasty one. You
could be tempted to check the result of the getTypeName command of list.at(0), but that is really
something you do not want to do.
Besides the additional typing and the problem with basictypes, the casting that is required does introduce a performance overhead. Yes, it is not very expensive and will be hardly measurable, but after-all there are additional operations that would not be needed for type-homogenous lists. And from a performance perspective, the best operations are those that are not there.
Also, there is no way of static error checking when you fill an objectlist that should only contain a certain type. So, if you plan on filling a list with points only, but accidentally add something else (which is one of the reasons why the cast-success-checks should be done) you are not pointed to it by the compile. Instead you experience run-time errors, which are way harder to identify and usually require quite some time of debugging.
Finally, all of the above also prevents us (i.e. FSYS "us") from implementing some convenient features
for FObjectList like sorting by name because not all objects have names (basic types do not).
So what's the alternative? We’d need a container that can be told in advance which type of objects it contains and can change its behavior based on that. This is where Generic-Types come into play! A generic type is actually a template for a whole range of other types. The types that are actually needed (e.g. list of double, list of points) are generated from that template once they are requested and are then readily available.
Generics in the Feature Programming Language
I already used the word "template". This is mainly due to my C++ background. Actually, templates in CAESES are more like generics in Java or C# because
- we only have generic-types (not generic commands which would be similar to template-methods/functions in C++) and
- we do not have generic Features (i.e. Features that allow template types – see below for a definition of this term – as arguments). But both of these could come, if there's a need.
So, what CAESES 5 offers is a range of generic-types that allow you to implement more efficient Features.
Of course, we need to talk about syntax. We (actually, I) decided to go with the same syntax that is used in many other programming languages that offer generics by using the larger and smaller operators. I figured that there must be a reason, why those very smart people that designed languages like C++, Java, or C# decided to go for that syntax, so I decided to go with the flow. For you this means that such a type is specified like this:
GenericType<T>
So for the already mentioned list type we'd have:
FList<T>
Or as a more concrete example:
FList<FDouble>
That declares a list with elements of type double.
Words
I hope that I did not lose you up to here. I do feel the need to clarify some vocabulary, for it to stay that way, though:
The vocabulary, I used in Effective FPL is still valid (finally I found an actual reason to make you read it). But there are some new words we need to define.
Templated type or just template
I use this to identify that a type accepts additional type parameters
(provided in angle brackets) and actually also needs them in order to become a complete type (a
templated type without specified types is an abstract type and cannot be instantiated).
Template type(s) or template argument(s)
Those are the types that are in between the angle brackets <> when declaring a specification of a templated type.
Template argument list
This is the full list of template arguments, so the complete, comma separated list between the angle brackets. Yes, a templated type may need more than one template argument and just like a command that takes multiple arguments multiple types for a template are separated using a comma.
Template specification
this is a templated type including the types that specify the types it should
work with (i.e. "templated type" + < + "template argument(s)" + >), e.g. FList<FDouble>.
Structure of this text
Once again (if you still didn't read Effective FPL , it's not too late) I will structure the text in separate
Items that each for their own should teach you a certain technique you can apply. However, I will not
only expect that you've read the previous items (which might be necessary to understand the line of
thought), but also assume that you are no novice to Feature programming.
Also, as opposed to Effective FPL , this time I will not stick exactly to the "teach one technique"- principal. This is due to the fact, that a lot of this text introduces new types of CAESES' class library.
So, it is some kind of mixture between class-reference, introduction to programming using certain data-structures, and technique-guide like Effective FPL.
Let’s get it on already!
OK, let's take a look at the templated types that are included in CAESES and how they can (and
should be) used. Please note that I usually use the placeholders "T", "U", and "V" to declare template
types. This means that, for this book, Flist<T> is a fully qualified template specification, which applies
to any type (you could replace T with) while FList denotes the unspecified (abstract type) that
actually stands for a range of types. Sometimes I divert from the T/U/V nomenclature if the template
type has a special meaning (you'll see what I mean).
Item 1 - FList<T>
To be honest, I think we've pretty much covered the main functionality of FList<T> in the abstract
example above. It's basically the same as FObjectList , except that you do not need to cast to an
expected type when accessing elements of the list and you will get a compile-time error when trying
to add types that don't match the list's template type.
Only read if the next two paragraphs if you know C++ and if you want to be very precise and need to
know all inner workings. In the C++ world FList corresponds to std::vector (not std::list!). FList does
offer a resize, and when applied the behavior depends on the template type: for basic types the new
entries are initialized to the basic type's null object (e.g. the number 0 for numerical types), for
others, the new entries are initialized as NULL.
However, as opposed to C++'s std::vector, FList offers operations that allow to remove or add list
entries at the front. If you are a C++ programmer, you will know that this doesn't perform too well
with std::vector (because the position of all elements in the vector need to be changed) but using
C++11's move-semantic this overhead is rather small, so I decided to go with std::vector for its overall
performance advantage. </C++ geekspeak off>
FList does have another advantage compared to FObjectList. If the template type is a sub-class of
FManaged , it offers a sortByName command, if it is a sub-class of FEntity , it also allow to sort by full
name (i.e. name including the scope(s) name(s)). Note that sorting by names is done using a "natural
compare" algorithm. This means, for example, that numbers are sorted according to their numerical
value instead of their string value.
If the list holds objects of a basictype, FList offers a sort()-command that will (for most basictypes)
sort the list by the values of its contained objects (more on that later). There are also other ways of
sorting an FList , but you're not quite ready for that bomb just yet (no offense).
An FList is convertible to the previously existing basic container types: FObjectList , FUnsignedSeries ,
FDoubleSeries , FUnsignedSeries , and FVector3Series if the template type matches. The following
table gives an overview of what conversions are possible:
FList template type Can be converted to:
T (any type) FObjectList
FDouble FDoubleSeries
FUnsigned FUnsignedSeries
FInteger FIntegerSeries
FVector3 FVector3Series
Of course, the foreach-statement works perfectly fine with the FList type as well. This already takes us to...
Item 2 - The auto-keyword
While this new keyword does not have anything to do with generics in general, it can be especially helpful when writing code that uses generic types. It just saves a lot of keystrokes when programming with them.
What is it good for? Specifying something as the iterator type of a foreach-statement which iterates
through an FList , that has nothing to do with the list's template type, does not make any sense. So,
writing something like this:
list<double> l()
// fill the list with doubles`
foreach (FCurve c in l)
// do something`
endfor
Will not only make no sense, it will also yield a compiler warning that states "the foreach-body will never be executed". I am still unsure whether this should actually be a compiler error, though, so at the time you are reading this, it might not compile at all.
Now, you may say, "If the compiler already knows that this doesn't make sense, why doesn't it do the
work for me?" Well, you are right and you actually can make it do the work for you. You can use the
auto keyword! When used in a foreach statement this keyword saves you the time of typing out the
iterator type. The compiler will automagically know what you meant (this technique is called type-
deduction). So, if you write
FList<FDouble> l([1.2, 3.4]) //oh, did I mention that you can initialize the `FList` like that?`
foreach (auto myDouble in l)
// work with "myDouble"`
endfor
The feature will be compiled just as if you would have written FDouble instead of auto. Now, for this
example, this might not save too much typing (3 characters, to be precise), but imagine iterating over
an FList of the following type: FList<FList<FList<FDouble>>>. Now we've already saved more key-
strokes than I feel like counting right now. And the declaration of those types can become arbitrary
long once we are talking about the other cool new containers (keep reading!).
The auto keyword also works when using the "old" container types, the types will be deduced as
follows:
| Container type | Deduced auto type |
|---|---|
| FObjectList | FObject |
| FEntityGroup | FEntity |
| FOffsetGroup | FOffset |
| FOffsetGroupAssembly | FOffsetGroup |
| FPanelMeshGroup | FPanelMesh |
| FStreamlineGroup | FStreamLine |
| FSurfaceGroup | FSurface |
I assume that list doesn't really include any surprises. But it shows that using auto with FObjectList or
FEntityGroup does not really give any advantage from just using a while or loop statement; you lose
the advantage of foreach of not having to cast your objects to the type you are interested in (see
Effective FPL for detailed information about foreach, while, and loop).
Now, the cool part is that the foreach-statement is not the only place where auto can be used. It can
also be used to create a variable of a basictype. In that case the type will be deduced from the
argument that is given to the creator. In turn, this means that a variable that is created using auto
always has to be initialized.
auto mystr("this is a string") // creates a variable "mystr" of type fstring`
auto dbl(0.2) // creates a variable "dbl" of type FDouble`
auto something() // creates a compile error, because the variable is not initialized`
point p() // create an object of type F3DPoint`
auto myP(p) // creates a compile error, because F3DPoint is not a basic-type`
Of course, the actual type of the variable is shown in the debugger and also when hovering over the variable with the mouse.
Auto is a nice addition to the FPL, however, it has some limitations. I already mentioned two of them:
it only works for declaring variables of basictypes and the variable has to be initialized with a value.
Now, here's the third one: it does not work if the variable initialization is done using an implicit type
conversion.
So, while
point p()
vector vec(p) // ok: point p is implicitly converted to type fvector`
Compiles and works, the following will not:
point p()
auto vec(p) // error: p is no basic-type object, implicit conversions are not done`
Let me explain why. Short version: It could be ambiguous! Longer version: In the point example it
actually could work because F3DPoint only knows one basic-type it can be converted to which is
FVector3. However, let's consider
parameter p( 42 )
auto d(p)
In this example we'd need an implicit conversion from FParameter to a basictype. Such a conversion
exists; a parameter can be converted to FDouble. But, that double is convertible to a whole bunch of
other basictypes: FUnsigned , FInteger , and FBool (after it was converted to an FInteger ). So it is
pretty hard to tell which type the programmer actually expects d to have. For that reason, this does
not work.
By the way, due to the same reason it does not work to declare an FList<T> using the auto keyword:
list<double> l1([ 1 , 2 , 3 ]) // works, because the given fobjectlist
// can be implicitly converted to FList<FDouble>`
auto l2([ 1 , 2 , 3 ]) // works, too, but the type of l2 is fobjectlist`
The same also applies when trying to initialize an fvector3 using auto:
auto vec([ 1 , 2 , 3 ]) // creates an FObjectList not an FVector3!`
While, the variable is called vec, it is actually an FObjectList and not an FVector3. For both of these
cases, the declared variable can actually be used as arguments for commands that take an FVector
or an FList (the implicit conversion will be done when passing the argument), but they do not offer
the commands the other types offer (so vec.setx(1) or l2.sort() would not compile, for example).
While it actually is the same thing, let me mention one more thing you might get puzzled about, when using auto. Let me start with a quiz. Given the code:
auto n( 7 )
What do you expect the type of the variable n to be? Obviously, there are three possibilities (let me throw in a fourth one, though):
a) Integer b) Double c) Unsigned d) None of the above
Time's ticking...
Ok. What's your choice? To some of you it might be a surprise, but the answer is b! Yes, we gave an
integer value, and even an unsigned integer value, but in CASES all numbers are treated as doubles
by default! (If you did answer d, please write me an email and explain that answer.) Of course, this
should not trouble you at all, because doubles can be used whenever one of the other two numerical
types is expected and vice-versa. I just felt the need to point it out in case you also know C++ (from
which we borrowed the auto keyword) where this would declare an integer variable.
auto in a nutshellautocan be used for the iterator type in a foreach-statement. The type of the iterator object is deduced from the container that is usedautocan be used to declare variables of basictypes, but those variables need to have an initializing value of a basictype that is not the result of an implicit type conversion. TheFVector3andFListexamples show that, while such code may sometimes be accepted by the compiler, it may not do what you'd expect.
Item 3 - Associative types and containers
Ok, this is not really an "Item" judged by the way I've used that term until now. It is more of an interlude, that I feel is necessary in order for you to understand the next items. So, I named it "item" to make sure that you do not skip the "boring" interlude.
With the introduction of type-safe containers it was possible to add a new class of types to CAESES
and those are associative types and especially associative containers. Associative types offer the
possibility to store mappings of two objects. Associative containers extend this concept to store a
collection of those mappings. Strictly speaking, the FList (as well as the previously existing list or
series types in CAESES) is an associative container as well, except that the key type is always
FUnsigned and it does not allow holes in the key-value-mapping. So, the big advantage, this
additional type of containers offers, is that the key can be any type and the mappings can include
holes (mathematical speaking this means, that if the container includes a mapping for value a and a
mapping for value b and there exists a value c with a < c < b, the container does not need to have a
mapping for c).
Now, this sounds terribly abstract, I know, but it's not that complicated, to be honest. Let me try to
clarify this by an example (if that doesn't help, I hope my description of the actual associative
containers below helps). Let's assume that FObjectlist would be such a container. Now, you all know
that the FObjectlist stores objects in a sequential matter, i.e. it associates an index value (of type
FUnsigned ) with an object (of any type). Now, let's further assume that FObjectlist would allow holes
in its association of index to object. Now, with those assumptions let's take a look at the following
code:
objectlist l()
l.insertAt( 0 , 9.7)
l.insertAt( 2 , 3.8)
What would happen here? We insert two objects of type FDouble into the list, one at index 0 and the
other one at index 2. We are missing an association to the value 1! So only these two accesses to the
list would be valid:
l.at( 0 )
l.at( 2 )
while
l.at( 1 )
would be an error (which needs appropriate indication and handling). That's all I wanted to say with the abstract description of "allows holes" above.
So, now that we know what associative containers do in general and what they are capable of, let's take a look at those that are available in CAESES, what they do and how they achieve their associations between values, and also what can and cannot be associated to each other (or at least may be associated differently from what might be expected).
Item 4 - FPair<T, U>
This very simple type stores pairs of the given types T and U. That's really all it does. Of course, you can access the values that are stored, change their values, replace them, do anything you like with them. I know, it sounds rather dull, but it can be a very useful type every once in a while. Also, it can be very useful when exploiting the capabilities of the new container types; however, we will come to that later.
Item 5 - FTuple<T, ...>
Now, this is a pretty special type (although its functionality is just as simple as the FPair 's). Notice the
"..." in the template argument list? This means that this type accepts an arbitrary number of
template arguments. Hä? (Excuse my German. For those unsure, this was an exclamation of
confusion.
What does it do? Just like FPair it stores a mapping of related objects, except that it is not limited to
store two objects, but as many as you like. However, you have to specify the types of the objects that
you want to store. So, the FTuple type stores at least one object of a given type, but it can also store
many more objects of different types, and that's what the "..." means.
In which cases could this help us? If you want to, you can think of a tuple as a class without a distinct name and without class-specific commands. It's a collection of objects that for some reason are stored together.
Now, this (just like FPair 's) does not sound too exciting (until you need it). So, let me go a little more
into the "..." part.
We call a template that accepts an arbitrary number of template arguments a variadic templated type or variadic template for short. Fine, but how can it be used and what's the big advantage? Since
we are still talking about the FTuple type, let's just go ahead and look at the relevant commands it
provides and how they behave: get and set.
Let's assume we have an FTuple object tpl of type FTuple<F3DPoint, FUnsigned, FLine>. This means that
it can store three objects, the first one must be a point, the second one an unsigned and the third
one a line. Now let's take a closer look at the two mentioned commands.
The set command takes two arguments, an index of type FUnsigned and an object. The specialty of it
is that the type of the second parameter changes depending on the value of the first one. So if we
pass 0 to tpl.set as the first argument, the type of the second one automagically changes to
F3DPoint. This means calling tpl.set(0, 1.2) will fail to compile, because the second argument needs
to be a point.
That's pretty neat, isn't it? To be honest, I am still a bit puzzled about how we can teach this to our auto-completion, but that's not a show-stopper (at least for me). The compiler catches it, and that's the more important part IMHO.
The get command behaves quite similar. It takes one argument of type FUnsigned and its return
value changes according to the value of that unsigned. So tpl.get(2) has the return type FLine.
Variadic templates in a nutshell- Variadic templates accept an arbitrary number of template arguments (must be at least one).
- They may offer commands with varying return or argument types depending on the value of another argument (which is always of type unsigned).
- The types of arguments may also change due to other reasons (we'll get to that later).
Interlude
I know it's not good manners to put "see later" in the nutshell, but in this case, believe me; it would not make sense to explain it here. Just trust me that it will have something to do with the types that you've passed into the templated type as template arguments.
Interlude part 2
I know what you are thinking: "what are other use-cases for such a variadic template type except for
the FTuple type, which might come in handy every once in a while, but surely does not justify the
implementation effort for variadic types?"
Well, first of all, let me assure you, that the implementation effort wasn't as high as you might think; once I managed to wrap my head around the idea, it was actually pretty simple.
Second, and more importantly, I did come up with other variadic template types that are far more
powerful than the rather simple FTuple , but I have to put you on the rack for this one. Let me tell
you: It may change the way you write Features forever (don't you hate cliff-hangers?)!
Item 6 - FMap<KeyType, ValueType>
Now, that's a type we've all been waiting for. If you weren't ... I was! What does it do? It maps a
collection of values of type Keytype to values of type ValueType.
The entries in the map are sorted by the KeyType. For the sorting the FMap type uses the "smaller"
operator, i.e. the result of <. To be quite honest, for many - actually most - types the result of that
operator is not really what a "normal" person would expect. While it does produce sensible results
for types like FDouble, FString, FUnsigned etc. (i.e. value-types which should be most types that you –
as experienced CAESES users – know as basictypes), it will compare memory addresses for more
complex types (e.g. FBSplineCurve ). For those complex types, this does not help for actual sorting
purposes, for mapping purposes it does, though.
Here’s some code for you:
fmap<FUnsigned, FDouble> m()
m.set( 9 , 4.1)
m.set( 1 , 3.1)
foreach (auto it in m)
echo(""+ it.second()) // let me get back on the ".second" later
// for now: it accesses the value that is stored in the map`
endfor
Will print
3.1
4.1
to the console because the map is sorted by the key’s unsigned value.
Now, this was pretty straight forward (once you've learned to ignore the .second() part inside the
foreach and accepted my comment - I will come back to it shortly).
What would happen, now, if the key wasn't FUnsigned but FBSplineCurve? To be honest, I cannot say.
The result may be either
3.1
4.1
or
4.1
3.1
But why? Well, the problem is that the FBSplineCurve has no way of telling whether it's smaller than
another FBSplineCurve. Of course, you could make up many criteria that would allow such a
comparison (length, starting point, ending point, for example), but none of those would suit
everybody. So, we decided, that every object of a type that is usable by you outside of Features (i.e.
any none-basic-type) will be checked for IDENTITY. Now, you may ask, how does identity offer a way
to determine an order? It's easy to see that it yields an equals operator, but how does it provide the
smaller operator?
To understand that, we have to know how identity is checked in CAESES. It is done by comparing the address in memory at which an object is stored. Since that address is nothing but a number it is then easy to extract an order from the identity check. Since the place in memory where an object is stored in memory does not change as long as an object is alive and no two objects can be stored at the same memory location, this allows to have a consistent identity check and sorting criterion. But it also means that the ordering may change the next time you open the same project because an object may (and most likely will) then be created at a different memory location.
The difference between the two types of sorting criteria, using the smaller operator of values for basictypes and the smaller operator of memory addresses for more complex ones, has another implication.
Let's take a look at the following code:
FMap<unsigned, double> h()
FMap<fparameter, double> g()
unsigned a( 8 )
unsigned b( 8 )
parameter c( 8 )
parameter d( 8 )
h.set(a, 1.23)
g.set(c, 4.56)
double dbl(h.get(b)) // what is the value of dbl now?`
double dbl2(g.get(d)) // what is the value of dbl2 now?`
Well, I already put the two quiz-questions into the comments of the code, now let me answer them for you.
The map h uses the basictype FUnsigned for its keys which provides a meaningful smaller operator.
This operator is used for sorting and look-up. So, even though we use two different unsigned objects
for inserting and accessing the values of the map, we will get the same value that was stored. So dbl
will end up with the value 1.23. For g things are different. While FParameter is a rather simple type
(all it does is store a value, or better an expression that can be evaluated to a value), it's not the
smaller operator of that value that is used, but the memory address of the FParameter object. Since
we use two different objects for storing and accessing, they will have two different addresses. This means that accessing the map using the second parameter will actually yield no value at all (well, since double is a basic type, it will return double's null value, which is 0). Now, you may ask "but parameter is such a simple type, why don't you give it a smaller operator that compares the values?" You are somewhat correct, but there's a major problem with that: the value of a parameter can change over time and if it does, the map may no longer be sorted! Here's an example:
// pre: assume that FParameter has a smaller operator that compares it's stored values`
point p( 0 , 0 , 0 )
parameter a( 9 )
parameter b(p.getx())
fmap m()
m.set(a, 5.3)
m.set(b, 1.2)
// now the content of the map would be [[b, 1.2], [a, 5.3]]`
p.setx( 12 )
// oups, we just broke the map, it is no longer sorted!`
Now, you might say: "easy, just re-order the map once one of the parameter's values changes.” Yes, that would (probably) be possible, but! There's a big implication this would have. If we would order the map by the parameters' values, the values would need to be evaluated on the fly whenever they change. This could trigger some CFD calculations, for example, that run for hours and you certainly would not want that. Even saying that only those parameters should be updated that do not trigger potentially expensive operations (as it's already done in the object editor to show values of expressions) does not help here because some parameters in the map may do so and others not. How should they be ordered? And in what way would such an ordering help you?
Even if there was a way to fix this case of a broken map, i.e. have some kind of mechanism in place that gets the list sorted again, what about the case of replacing the 12 with a 9? We'd now have two entries in the map that contain the same key-value and there is no way to fix that. The map cannot know which of the two mappings to keep.
So, we'll stand to our decision that a map with a key type that does not allow a meaningful smaller- than-ordering are ordered by the key objects' memory addresses, even if it means that the ordering is not consistent when re-opening the project.
FMap in a nutshellFMap<K, V>is a container that can hold mappings of keys to values.- Mappings are sorted by the key. This sorting is only consistent and meaningful for "value- types", for others it is based on addresses in memory that will change when re-opening the project.
- Mappings are unique. There cannot be multiple entries with the same key.
Item 7 - Foreach and FMap
Yes, we are still talking about the same container type, but compared to other containers, foreach
does behave special for maps, so I decided that it's worth an item; especially since FMap is not the
only container this item applies to, but I will introduce you to the second one later.
I told you that the foreach-statement is usable for all new containers. You may think "how can this be
done for a map? It contains two objects for each entry, so what do I get when I type foreach(### entry
in myMap)? What can I replace the ### with?" Well, let me solve the riddle. It's actually a type we
already talked about: FPair. Well, that's not totally true, though. It is a type that behaves just like an
FPair with a small exception: it does not allow to change the first object in the pair (because that is
the key in the map and we already agreed – well, you may not agree, but Stefan and me do – that
changing the key in a map is a no-no because it might break the map). So as opposed to FPair it does
not have a setFirst command. Additionally, if the first object (i.e. the key of the map) is a basictype,
you cannot call commands on the first object that would change the value of the object (because
that value is used by the map to do the sorting).
While the first restriction is rather simple to understand, I think an example may help for the second:
fmap<F3DPoint, fline> m1()
fmap<double, fline> m2()
// fill both maps`
foreach (auto it in m1) // using auto as the iterator type`
it.first().setx( 8 ) // set the x-coordinate of all points that are keys in the map to 8
// ok: it changes the value of the object that is first in the pair, but F3DPoint is no basictype
// so the value is not used for sorting of the map`
endfor
foreach (auto it in m2)
it.first() *= rand() // try to multiply every double object that is a key of
// the map with a random value
// error: object that is returned by first cannot be changed`
Endfor
So, I think the comments say it all, but I also think that this example really shows the reason for this restriction. If the second foreach would be accepted by the compiler and would actually do what is written there, it would be highly unlikely that the map keeps its sorted state. We don't want that and I can assure you that the underlying C++ container would really give us hell if we'd allow it.
Oh, by the way, the second part of this restriction is realized through the concept of const- correctness. I wrote a (rather long - but you probably expected that, since you may know me by now)
blog post about it. So if you are unsure about what this means, just take a look at that post. It is
definitely worth looking into because it might affect some of your Features that are currently
working.
So, where was I and what was I aiming at.... ah, right, FMap and foreach. I told you that the objects
that you get in a foreach over a map are some type of FPair , but not really. The beautiful name of that
special type is FIteratorPair<T, U>. Let's take a look at an (very ugly) example of what this means if
you want to spell out a foreach over a map:
FMap<F3dpoint, Ftuple<Funsigned, Flist<F3dpoint>, Fline, FsurFace>> m() // this already looks nasty`
... // Fill the map`
foreach (FIteratorpair<F3DPoint, FTuple<FUnsigned,FList<F3DPoint>,FLine,FSurface>> iterator in m)
// it didn't get prettier
// foreach body, access keys and values of the map through iterator.first() and iterator.second()`
endfor
I admit: this is ugly. BUT! Remember the auto keyword? This is the perfect place to use it! So, as I did
in my previous example, all you need to type for the foreach-part of the code is
foreach (auto iterator in m) // now this doesn’t look too bad
// foreach body, access keys and values of the map through iterator.first() and iterator.second()`
endfor
Now, that is a case where the auto - keyword really saved a lot of typing (or copy-pasting) isn't it?
Unfortunately, it does not spare us from typing out the map's type in its declaration, and I am yet to
come up with a solution for.
Foreach and FMap in a nutshell- The iterator type of the foreach that does not allow to set its first element.
- For basictypes, it is not allowed to call commands on the object that is returned by
first()that would alter its value - The
autokeyword is really helpful here.
Item 8 - Using an FMap
I have yet to answer an important question: "How do I actually use an FMap - container?" And just as
important (maybe even more important): "How do I use it efficiently?"
Adding elements to a map is pretty straight forward using one of the set-commands. One takes two
parameters, the first one being the key and the second one the value. The second command takes an
object of type FPair with the key as the first entry of the pair and the value the second one. So the
two (full) signatures for those commands are:
FMap<K,V> FMap<K,V>.set(K key, V value)
FMap<K,V> FMap<K,V>.set(FPair<K,V> mapping)
Both versions of the set command return the map object itself, which allows convenient inserting of multiple mappings in a single line.
But now what? We've already seen that we can iterate through the map using foreach, but that's not very efficient if we are interested in a certain key-value mapping.
We can access a value that is stored in the map using the
V fmap<K,V>.get(K key)
command which returns the null-value for the stored value-type if no mapping for the provided key
exists. For non-basictypes this value can be tested for NULL using the not-operator, but what about
basic-types? If V is FDouble , we will receive a valid FDouble with value 0. There are multiple ways to
solve this. I will start with the least efficient one (which you should not use) and end with the most
efficient one.
Option 1: Look at the keys.
FMap<K, V> offers a getKeys command that returns an FList<K> which
contains all keys that exist in the map. You could then search the list to check the existence of the
key. Now, as I said, this is far from efficient and you should not do that. Of course, you could use your
knowledge that the keys are sorted and implement a binary search, but please still don't do it.
Option 2: Use contains.
FBool FMap<K,V>.contains(K key)
command tells you, whether the map contains a mapping for the given key. Now, while this is far more efficient than searching the list that is returned by getKeys, it still involves two look-ups in the map: first the look-up for the contains-command and then (provided that contains returned true) the look-up for actually accessing the mapping. While those look-ups aren't extremely costly, it is still nice to not do more than is needed. So, we'd need a command that combines the two commands into a single one, which brings me to...
Option 3: Use the find-command.
The find command does exactly, what its name says, it finds the mapping for a given key. "But that's exactly what 'get' does, isn't it?" Yes and no. Essentially both commands do the same, but the return value of find is not the value directly, instead it is a type that we've already talked about. Let's take a look at find's signature:
FIteratorPair<K, V> FMap<K,V>.find(K key)
So, it uses the same type that is used for foreach'ing over an FMap. If you are unsure about that type
with that beautiful name, just scroll up...
The getFirst command of the pair returns the key (which is kind of redundant, because you already know it - you did pass it to the find command to begin with) and the getSecond command will return the mapped value.
But, how does this solve the question whether a mapping existed to begin with? The FIteratorPair is
a basictype as well (all templated types are, by the way), we cannot check that for NULL, so we have
the same problem as with the get-command.
One thing I didn't tell you about the FIteratorPair type is, that it also offers an FBool isValid()
command. I didn't tell you because inside a foreach that command will always return true. Not so for
the result of find. If the key was not found in the map, this command will yield false. "First" will still
be set to the argument you passed into find, but "second" will contain the value-type's null-value.
Now, that allows to not only check if a mapping for a certain key exists, but it also allows to directly
work with the mapped value.
Yes, storing the return value of find into a variable requires quite some typing, since you have to not
only type the word "FIteratorPair" but also the template arguments of your map. If only there was a
way this could be circumvented... oh, right, there is the auto keyword! Just type auto it(myMap.find(key)) and you're done!
FMap in a nutshellFMapprovides two versions of set commands to insert mappings.FMapoffers a getKeys command that returns anFListwith all keys that are present in the map. Similarly, it offers a getValues command.FMapoffers a contains command that tells you whether a mapping exists for a given key.- The most efficient way to access elements of a map is to use the find command which makes use of the
FIteratorPairtype.
Item 9 - FHash<KeyType, ValueType>
FMap is not the only associative container type that comes with CAESES. The second one is FHash<K,V>. It has a lot in common with the FMap type. Both take key values of a certain type and associate them with a value of a certain type, after all that's what associative types are all about. They also share the same command-interface and utilize the FIteratorPair type for it.
The difference is that the entries are not sorted by the smaller operator of the KeyType. Instead a hash-value is calculated from the value that is provided for the KeyType. Mappings are then done based on that value. In general, this allows a better performance when inserting new mappings and accessing existing ones.
However, this technique shares a limitation with the FMap type: it uses identity for non-value types. For value-types the hash value is calculated from the value, for others it is, again, calculated from the object's memory address. So, it behaves just like the FMap type: when accessing a mapping with a value-type object that was created with a different object of the same value-type, you will get the stored value. This is not the case when two different objects of a non-value type are used for the key.
This also means that the FMap will basically behave like an FHash for non value-types because the sorting of the FMap will just be (well, actually, I would have to type "seem") as random as for the FHash.
The foreach-statement behaves exactly the same for FHash as for FMap , so I will skip this here.
FHash also offers the same set and access commands as FMap , including the find-command that,
again, returns an FIteratorPair object.
FHash<KeyType, ValueType>FHashbasically serves the same purpose asFMap, mappings are not sorted by key, though.FHashhas better performance thanFMap, use it instead ofFMapwhen sorting does not matter.- Use
FHashinstead ofFMapwhen the KeyType is not a value-type
Item 10 – FVectorN
The FVectorN is a collection of values of a given type. So, basically, it is a list of values of a given type.
You might ask “Well... but ... don't we already have Flist<T>? Why another one of those?” and you are
almost right. In fact, with all the sorting and filtering stuff that is provided by FList , it is far more
powerful than FVectorN. However, before I go any further in trying to explain why this type exists,
let's take a look at the complete template type definition of FVectorN :
FVectorN<T, FUnsigned>
Ok, there's something strange here. First of all, there's the type of the elements in the vector. That's hardly a surprise. But there is a second template parameter, and also I seem to have made an error there, the second parameter is no place-holder, it already is a type. “Did you copy that from one of your testing projects and forgot to change the second parameter?” No, this is no copy-and-paste error. It means that the second parameter expects a number as its argument, an unsigned number to be precise which specifies the dimension of the vector.
So, in other words, for FVectorN the dimension is part of the type. Now, you might ask „but why?“
One simple and very valid reason: to find programming errors as soon as possible, i.e. at compile
time. By distinguishing between vectors of different dimensions at that early stage, it is possible to
restrict certain operations with the vectors by the compiler which leads to fewer run-time errors.
When run-time errors occur it almost always means that you have to spend time debugging your
code. And it’s really bad if those errors do not occur during initial testing, but in actual productive
use, e.g. when running a variation with thousands of designs where half of them fail due to a small
oversight when developing the central Feature of your geometry creation process.
Please note, that in the upcoming text I may replace “ FUnsigned ” with a lower-case „n" when I need
to type out the templated type FVectorN (or any other possibly upcoming similar types).
So, I must interrupt, but this is important: the over-all nutshell for templated types has just been extended by the fact that, contrary to previously spread information (in this document), template parameters do not necessarily need to be types, they can be values as well, i.e. basictypes. Please note that they really need to be actual values. You cannot declare such a type based on commands or variables. In other words, the following will not work:
FUnsigned i(0)
FVectorN<double, i> vec()
And the same goes for
Point p([3,3,3])
FVectorN<FDouble, p.getx()> vec()
A type is a type; it needs to be known at compile-time and therefore it cannot be depending on a command which would be evaluated at run-time. Otherwise, we'd throw all the nice type safety we've accomplished right out the window right there.
„Ok, fine, you've convinced me, maybe it is a good idea, so how do I use FVectorN ?“
Creation of a vector is similar to creating any other (templated) type:
FVectorN<double, 4> vec()
Simple as that. Now vec is a four-dimensional vector that can hold double values, which brings me to
the next point: how is the vector initialized? Since the vector has a size right from the beginning
(unlike FList ), the elements need to be initialized in some way. In this respect, FVectorN behaves
exactly like the resize() command of FList : if the element-type is a basictype, the elements will be set
to the type's default-value (e.g. 0.0 for FDouble ) and if it's no basictype, the elements will have the
value NULL.
You can set elements in the vector by using the set-command:
void FVectorN<T, FUnsigned>::set(unsigned index, T value)
You can also initialize the vector at creation time using the “[“ and “ ]” notation:
FVectorN<FDouble, 4> vec([1,2,3,4])
Of course, in this case the dimension of the vector must match the number of elements in the given list, otherwise the compiler will complain (type safety at work again!).
Elements of the vector can be accessed using the at-command:
double d(vec.at(2)) // d = 3
Ok, we know now, how to create, fill and access a vector, but there must be more. Of course there is. You can do all sorts of vector operations with it. For example, you can multiply two vectors:
FVectorN<FDouble, 3> v1([3,3,3])
FVectorN<FDouble, 3> v2([2,2,2])
FVectorN<FDouble, 3> result(v1 * v2) // result will have the values [6,6,6]
Add vectors
FVectorN<FDouble, 3> v1([3,3,3])
FVectorN<FDouble, 3> v2([2,2,2])
FVectorN<FDouble, 3> result(v1 + v2) // result will have the values [5,5,5]
and all kinds of other nice things. And those nice things bring me back to the reason why we chose to include the vector's dimensions into the type: type-safety!
Take the following code:
FVectorN<FDouble, 4> v1([3,3,3,3])
FVectorN<FDouble, 3 > v2([2,2,2])
auto result(v1 * v2) // error: no operator * defined for types
// FVectorN<FDouble, 4 > and FVectorN<FDouble, 3 >
If we didn't include the vector dimensions into the type this would compile nicely. However, it would yield no meaningful result (what are the dimensions of the resulting vector, what happens with those elements of vector v1 that do not have a corresponding element in v2?). So, due to our decision the code above will not compile in the first place, so you do not run into trouble at run-time.
Ok, basically, this concludes the chapter about FVectorN. Maybe it's interesting to know that an
FVectorN<FDouble,3> is exchangeable with an FVector3. FList s can be converted into vectors if the
number of elements matches the dimension of the vector and the element type matches.
FVectorN- Templates can have value type parameters
FVectorNis a container of elements of a certain type with a predefined size- The dimension of the vector is part of the type
FVectorNprovides several vector operations.FVector3andFVectorNare exchangeable if the type isFDoubleand the dimension is3
Item 11 - FArray<T, FUnsigned, FUnsigned, ...>
Ok, we've digested the FVectorN type by now and the fact that template parameters may be of a
value-type. However, all of that was pretty much a build-up for this type. It is the Type that we
received the highest number of requests for by users, I believe. So, now you have it: N-dimensional
arrays!
The type declaration in this item's heading should not pose a problem for you to decipher anymore after everything you've read so far. However, I will go gently on you and take it one step after the other:
FArray<T, funsigned, funsigned, ...>
First of all we have the type T that specifies the type of elements in the array.
Then we have two template parameters that expect an unsigned value, just like we've seen for
FVectorN , except that we have two of those here.
Finally, we see “...” which means it's a variadic template. What should we fill in there, now? Well, to
be honest, I couldn't come up with a good way to express this in the type's abstract definition, so I
will just tell you. All of the variadic arguments to the template need to be unsigneds that specify the
size of the following dimensions. So, an FArray needs to have at least two dimensions (otherwise it
would be a vector) but it can have as many dimensions as you wish. If we ever come up with a
variadic template type that mixes value-parameters and type-parameters in its variadic part, I really
have to come up with a meaningful nomenclature.
To be honest, thinking in three dimensions already causes my head to rotate, but maybe you have a reason to store 4-, 5 - , or even 500-dimensional data. I must admit, in the 500-dimensional case, it will be a real pain to type out that type-name, but if you really need to use such a type, I guess that is the smallest of your worries.
Ok, now that we have that figured out, let me tell you a little secret: the type we've looked at before,
FVectorN<T, n> , is actually nothing else than an FArray<T, n, 1> and can be used everywhere where
you could use such an FArray. I know, you don't even know yet, where and how you can use any
FArray , I just felt like sharing.
Fine. So, the type
FArray<double, 4, 4>
Defines a 2-dimensional array (so a matrix) with 4 rows and 4 columns.
Let's look at that type-safety thing again. Having the first 4x4 matrix in place, the following type
FArray<double, 5, 2>
is not compatible (regarding assignment or copying) with the first matrix declaration. The dimension of the matrix matches (2), but the size of the dimensions doesn't (4x4 vs 5x2). By including the dimensions in the type declaration, we have, again, ensured that the following will produce an error at compile time (instead of run time):
FArray<double, 4,4> a()
// fill the matrix
FArray<double, 5,2> b(a) // error: type mismatch
So, again, the reason for all this hassle and the “funky" syntax is actually to make your life easier. Once more: every programming error that can be caught at compile time instead of at runtime can save a lot of time (and money, for that matter).
Copying is the simplest example where this can help catching bugs early, but it definitely helps when we talk about array (matrix) operations, where it might be harder to catch the bug at first glance.
First of all, you want to see how you can use the FArray class, right? Ok, let's start with the most basic
three operations: creating an array, setting data in the array, and getting data from the array.
Creating is the same as always (see the examples above), but just like for the FVectorN type, there is
a difference to the initialization of other container types as the elements need to be initialized at
create time. However, it behaves exactly like a vector. For basictypes the array is filled with the
corresponding default value, for other types the elements are initialized to NULL.
Now, how can we set an element in the array? Well, we use the set-command, duh! Here's the signature:
void FArray<T, n, n, ...>::set(FTuple<FUnsigned, FUnsigned, ...> index, T value)
Now, this needs some explaining, I guess (don't worry, the actual usage is pretty simple). We now see
a command of a variadic template type that gets an object of another variadic template type as an
argument. In this case the number of template arguments for the tuple is the same as the dimension
of the array. So it corresponds to the number of unsigned values given to the array-template.
Additionally, the template arguments for the FTuple are all FUnsigned. I just didn't write that out in
the signature to avoid confusion with the value-type template arguments. It's hard to put this into
such a “formalized” signature format as above, so, as usual an example may help:
FArray<double, 4, 4> matrix() // a 4 x 4 matrix
FTuple<unsigned, unsigned> idx([0,1])
matrix.set(idx, 42) // sets the value of the double in the first row
// and the second column of the matrix to 42
I hope this is pretty self-explaining. The tuple contains the index for every dimension of the matrix. Don't worry, it doesn't always have to be as verbose as above, you can also write:
matrix.set([0,1], 42)
to get the same result. This is another example where the decision to include the dimensions of the array in the type comes in handy. The actual signature of the set command of our object matrix is:
void FArray<FDouble,4,4>::set(FTuple<FUnsigned, FUnsigned> idx, FDouble value)
So the following would not compile!
matrix.set([0,0,0], 42) // error, invalid type for first argument!
Now, I hope you are starting to fully understand (in case you still had doubts) why including the dimensions into the type was a good idea.
As an alternative to use the set-command, it is possible to initialize the array using nested objectlist- syntax at creation time:
FArray<FDouble, 2, 2> arr([[1,1],[1,1]]) // creates a 2x2 matrix filled with ones.
Well, the at command works exactly the same way:
T FArray<T, n, n, ...>::at(FTuple<U, U, ...> index)
So, assuming the code above was executed, the following would print 42 to the console:
FTuple<FUnsigned, FUnsigned> idx([0,1])
echo(""+matrix.at(idx))
Or shorter and easier on the eyes:
echo(""+matrix.at([0,1]))
Regarding the examples of at and set above, there's something I've left out. For two-dimensional
arrays there are convenience versions of those two commands that take two FUnsigned s instead of a
tuple. They are far more efficient than the generic tuple version, so you should rather stick to them
whenever you are dealing with matrices (which, I assume, will be the most common use-case of the
FArray type):
void FArray<T, n, n>::set(FUnsigned row, FUnsigned column, T value)
T FArray<T, n, n>::at(FUnsigned row, FUnsigned column)
As I said, those two commands are only available for two-dimensional arrays, so the following will produce a compiler error:
FArray<FDouble, 2, 2, 2> arr() // three dimensional array
arr.set(0, 0, 42.0) // error: no command “at” defined with arguments FUnsigned, FUnsigned, FDouble
There are many operations defined for the FArray type and I will not go into detail for all of them.
Have a look at the documentation inside of CAESES to learn all about them. One thing I'd like to
mention, though, is that similar to the specialized at and set commands I just mentioned, there are
several commands that are defined for two-dimensional arrays only, for example matrix-
multiplication. They are all included in the general FArray documentation but will be marked with the
conditions that need to apply for the command being available.
This may not only include conditions regarding the array‘s dimensions, but also the type of the elements (matrix multiplication is only available if there is a multiply operator and an add operator is defined for the element type for example).
FArray in a nutshell- The
FArray<T, FUnsigned, FUnsigned,...>type implements multi-dimensional arrays FArraysare compatible withFVectorNsinceFVectorNis just a two-dimensional array with the second dimension being set to one.- Elements of the array can be accessed using an
FTuplewith as many entries as there are dimensions in the array. All elements of the tuple are of typeFUnsigned. - There are special commands available for two-dimensional arrays (matrices) depending on the element type.
Another interlude
Ok, so far, I think, there wasn't anything that should scare you off, right? Yes, the funky syntax of
generic types using the angle brackets might take some time of getting used to, but it’s nothing that
should cause wet pants. Seriously, once you've gotten used to it, you don't even see the angle
brackets but read " FList<F3DPoint> " as "list of points" without thinking twice. At least that's how it is
for me with generic types in the programming languages I know, so your results may vary.
Anyways, let's move on to the funky stuff (disclaimer: as I am writing this line, I am not 100% sure how to implement this stuff, but I will).
Note: I will revisit the following paragraphs many times, but I will keep the disclaimer above - yes I am writing this whole thing before it's even completely implemented, but I am so excited about it! Also, it has the advantage that you can start off with those cool new types right away, because you already know all about them.
Let me finally take you off that cliff I've been leaving you hanging on since the second part of my
interlude after Item 5 (the FTuple type).
The implementation of generics did not only open the door to convenient and efficient to use type- safe containers (I know, programmers are all about efficiency). It also brought something up to the daylight that was lurking in the shadows up until now. We were thinking about it and just didn't
know how we can offer it to you. But now it arrived: the possibility to store and use commands in a variable.
I know, it sounds strange, and at first might be hard to grasp for you, but commands have become first level objects. If you don't know what that may mean, don't worry, just keep reading.
Let's face the monster that is called FCommand!
Item 12 - DEDUCING FCommand
First of all, now we really have to watch very closely at some words: in a previous article I said that I
sometimes leave out the leading "F" that is common to all types in CAESES. Now, I will never do that
for the FCommand type. We have to distinguish very exactly between the word command (which is
an operation that can either be executed on an object or globally) and the word FCommand (which
is an object that can store such an operation for later use). MOVE DOWN TO UNDERSTAND THIS (if
you do not do so already)!
What is a command? Well, I already defined it in Effective FPL , so I am not going to do it again. If you haven't read it, do so now!
Ok, in short (and only so we are on common grounds), a command is an operation, and it can be
either global or it can be executed on an object of a specific type. Oh, I just realized, I failed to define
a word with regards to commands (also in Effective FPL ): overloading / overloads. Sometimes, there are commands that have the same name, but take different types as parameters. Those commands are “overloaded“. This means, you can call them using the same name, but using different arguments
and the compiler will know which command to use based on the argument types. However, there are
no commands with the same name and the same arguments that differ regarding the return type,
only. Why? Well, even if a command returns a value, there is no need to actually use that value. It's
perfectly fine (however, pretty pointless) to write the following:
point p()
p.getX() // call getx, but don't do anything with the returned value
As I said, that code is completely pointless, but it serves the purpose to show why there can't be overloads based on a command's return type. Let's assume, this was possible and the following versions of getX would exist:
FVector3 F3DPoint.getX()
FDouble F3DPoint.getX()
Now, given those two commands, which one should be taken in the (nonsense) code above? There's no way to tell, so overloading based on return type only is impossible.
This is just for completeness, and also because we are going to see some overloaded commands in the near future (plus I can reuse that paragraph if I actually decide to write a prequel to Effective FPL that explains the basics of the FPL).
So far, so good.
We call the object a command can be executed on "the receiver". For global commands this receiver
has the value NULL (or null, Null, nUll - the FPL is not case-sensitive) – there is no receiver (BUT! there
still needs to be a type-representation for it in CAESES, so the compiler can handle it, but I will get to
that later. Really.)
Additionally, a command has two major properties: the return type and the arguments.
So, let's abstract from there. After all, generic types are all about abstracting from the concrete types
that are used. I already spilled the bean that FCommand is a generic type, so we'll take it from there.
Now, we've agreed on the fact, that a command needs one type: the type of the value it returns. Some commands do not return anything, so that case must be handled somehow.
Finally, we have to find some abstraction for the arguments a command may need that part depends on the specific command.
Let's put these together, taking generics into account. All of the above leads us to three observations for a generic type that could represent a command:
- A command needs one template argument for its return type
- A command needs one template argument for the receiver type
- A command needs template arguments that represent a list of types for its argument types (which might be empty if the command does not take any arguments).
Now, how do we tie these three observations together in an actual type declaration?
Well the first two are rather simple to put in practice. Suppose we have a templated type
" FCommand " (surprise!). It definitely needs two template arguments, one for the return type and the
other one for the receiver's type. This means, so far, we have the type declaration
FCommand<ReturnType, ReceiverType>. Nice. The ordering of these two types, by the way,
corresponds to the ordering of types when looking at a command's signature (e.g. Fdouble F3DPoint.getX()).
Now, what about the arguments? How should we now the number of arguments? Well, we don't.
Remember FTuple? Yes, you do (well, if you don't, I am disappointed because you didn't really read
this document)! So, all I can say is: Variadic templates to the rescue! Yes, the FCommand type is a
variadic template. This allows it to take an arbitrary number of types that specify an arbitrary number
of arguments (depending on the command that should be stored in the FCommand object - don't
worry, examples will follow). If the command doesn't take any arguments at all, it needs are the
receiver's type and the return value's type.
Now, let's take a look (and maybe two more) at the complete name of the FCommand type:
FCommand <ReturnType, ReceiverType, ...>
Isn't she a beauty?
Ok, I already said that the receiver of a global command is NULL. However, the declaration above
needs types and NULL is a value (e.g. when declaring F3DPoint pointRef() the variable pointRef will
be of type F3DPoint, but its value is NULL). So, there actually is a special type for that in CAESES:
FNull. This means, the ReceiverType template argument is FNull for global commands. The same actually applies for the ReturnType, if we have a command that does not return anything. To make it easier on the eyes and closer to things programmers are used to, you can actually use the word “void” instead of FNull in this case.
"Fine, I Completely lost you now, and how can we use all of this?"
Don't panic, it's ok if you couldn't quite follow. I just hope that I can shed some light in the following paragraphs.
Item 13 - Using FCommand with global commands
I'll start with a simple example of how you can store and execute a global command in a variable:
FCommand<FNull, FNull, FString> simpleCmd()
simpleCmd.set(echo(FString))
// we just stored the global command "echo" into the variable "simpleCmd"
// now, let's execute it`
simpleCmd.execute(`NULL`, "Hello World")
The code above will print the String "Hello World" to the console. Exciting, isn't it? Now, this
wouldn't be exciting, if we didn't achieve it through an object of type FCommand!
Let's take a closer look:
The type declaration
FCommand<FNull, FNull, FString>
surely deserves to be looked at. The FCommand-part shouldn't surprise you. After all that's what we are
talking about here. The template arguments do deserve a closer inspection, though. Remember that
the first template argument specifies the return type. The echo command does not return anything,
so this is FNull.
Now, echo is a global command, so there is no receiver type, i.e. we have to set that to FNull , as well.
Finally we've arrived at the most interesting part: the parameter of the command. The echo-
command takes exactly one parameter of type FString. This is specified by the first parameter of the
variadic part of the FCommand type. So, the third template argument FString specifies, that the
execute command of the FCommand object simpleCmd takes one argument of type FString.
Now that we have all of that in place, we have to take a look at another property of variadic
templates that I omitted in my explanation, when I talked about FTuple. Commands of variadic
templates may vary the number of arguments of certain commands depending on the number of
variadic template types.
In the FCommand case this corresponds to the number of arguments the command that is stored
takes.
Confused, yet? Well, let's re-visit variadic templates... I said that all variadic templates need to have
at least one template argument. However, some may require more. That is the case for the
FCommand type. It requires at least two template arguments: the receiver type and the return type.
Every type declaration in between the angle brackets of the FCommand type that follows the
receiver type is considered to be the variadic portion of the type. If the type is not a "pure" variadic
(like FTuple is) this may also be zero additional types. So, while it would be illegal (the compiler will shoot you down) to write FTuple<> tpl(), it's perfectly fine to write FCommand<F3DPoint, FDouble> cmd(),
although the second declaration has only two types and no types for the variadic portion of
FCommand.
Ok, now that we have sorted out that part of the story, let's take a closer look at the execute
command of the FCommand - type.
One thing is for certain: its purpose is exactly what it says: execute the command that is stored in the
FCommand - object. The first parameter of the execute command is the receiver object of the
command. Since we are talking about global commands right now, we pass NULL here. Now we have
to somehow provide the arguments for the command's parameters. They can be passed directly to
the execute command because the types and number of arguments that you can pass to the "execute" command change depending
on the variadic types of arguments you have supplied in the type declaration.
So, the nutshell for variadic templates has just been expanded by that item.
FCommand- When using
FCommandto store a global command the second template argument for the receiver type needs to be set toFNull. - When using
FCommand.executeto run a stored global command the valueNULLneeds to be passed as the first argument to execute since it represents the receiving object. - When using
FCommandto store a command that has no return value the first template argument for the return type needs to be set toFNull. - The variadic part of the
FCommandtype corresponds to the argument types of the command that is stored.
Item 14 - FCommand and member-commands
Yes, I know, we are very deep inside of the "what(tf)-are-you-talking-about”-land. Let’s try to convert this confusion to comprehension, by using an example that offers more than the previous – global command – example.
So, let’s look at the getDisplacement command for the type FsurfaceGroup. It takes an object of type
FsurfaceGroup as the receiver, it returns a value of type Fobjectlist (whatever that may mean, but
that’s beyond the scope of this text) and it takes multiple arguments of several types. Let’s take a
look at the Fcommand – specification that could store the FsurfaceGroup.getDisplacement command:
Fcommand<FobjectList, FsurfaceGroup, Fdouble, Fdouble, Fdouble, Fbool, Fdouble, Fdouble, Fdouble,
Fdouble> // wow!`
We’ve already established the purpose of the first two template arguments: they are the return type and the receiver type of the command. Everything that follows are arguments to the command and for the getDisplacement command, that’s quite a lot of arguments. I will not follow through on the example of the getDisplacement command, to be honest, it is just too much typing, but I hope you get the idea. You’ll get a working example in a minute, I promise.
Now, the compiler makes sure that whatever you provide as a command to the FCommand - creator
matches the specified types. So declaring FCommand<Fnull, F3DPoint> cmd(F3DPoint.getx()) will produce
a compile error, because no command getx exists that has the type F3Dpoint as its receiver and the
specified return type ( Fnull – i.e. it returns nothing). So the correct syntax would be
FCommand<FDouble, F3DPoint> cmd(F3DPoint.getx())
This stores the getx-command of the type F3Dpoint in the variable cmd.
Here you see something that bothers me quite a lot and I hope that we can change it: in order to
pass a command to the FCommand object you have to type out the complete signature of the
command (excluding the return type). You need to include the receiver type, although it is redundant
(you already specified it in the template types for the FCommand type) and you need to type out all
argument types (although you already had to type them once before). So I hope we can reduce this
to only having to type the command’s name (i.e. getx for the example above). We’ve already got rid
of a lot of unnecessary typing that was introduced by generic types thanks to the auto - keyword, so I
hope we can get rid of that, too.
Anyways, all future optimizations aside, how can cmd be used? Easy, just specify the receiver object as the first argument and the arguments to the command after that.
So, we could call F3DPoint.getx() like this:
Fcommand<FDouble, F3DPoint> cmd(F3DPoint.getx())
Point p( 1 , 2 , 3 )
Double d(cmd.execute(p))
The result of this code is that d holds the value 1.
FCommandcan store member commands by passing the receiving type of the command as the second template argument (receiver type).- The actual object that the member command should be executed on is passed as the first argument to the execute command.
Item 15 - FCommand in the real world
Now, while I think that at least some of you can already come up with nice use-cases for the
FCommand type, let me give you a very concrete one that involves the new container type FList<T>.
Ok, I already mentioned that there are ways provided to sort an FList<T>. Besides the possibility to
sort it by name or full-name (if the type T is either FManaged or FEntity ) it also offers a sort
command if the type that is inside the map is a basictype (otherwise that command is not supported
by FList. It will not even compile). Now, it's the time to talk a little bit more about that command.
Remember the chapter about FMap? FMap keeps its content sorted. For non-value-type this sorting
is done using memory addresses, which is not really useful for you. However, for value-types sorting
is done by their actual values. I already said that value-types in CAESES, for which this applies, are all
basictypes. Remember, though, that it does not apply for all value types (we'll provide a list that we'll
keep you up-to-date in the documentation). So, this means that calling sort on an object of type
FList<FDouble> , for example, will actually create a sorted list of doubles! Sorting is done in ascending
order and it will definitely provide better performance than implementing your own sorting
algorithm as functions inside the feature (or use one of the implementations that I provided in our
forum).
Now, what if sorting should be descending? Ok, you could call sort and then reverse the list, but what
about sorting a list of non-value-types? For example, if you need to sort an FList<F3DPoint> by the y-
value of the points it contains? Of course, you could go back to using the sorting implementation in
feature code, OR, you can utilize the FCommand type and use one of the generic sort commands that
FList offers. Whoa! Now that sounds a little exciting, doesn't it? Yes? Well, I agree, so let's take a look
at how that works.
Additionally to the three sort commands that I already mentioned that depend on the template type
of the list, there are three generic commands that each take an FCommand object as an argument,
let's take them one by one.
The signature of the first one is
FList<T>.sort(FCommand<FBasicType, T>)
Let's dissect this signature. The receiver type is FList that can hold objects of type T. The argument is
an FCommand - object that can hold commands that take a receiver of the list's type T and return an
object of the type FBasicType. The big question is: how does this FCommand help in sorting the list?
Most of you have probably figured it out pretty quickly, but let me still give you the answer: during
sorting that FCommand object is executed for the elements that are contained in the list. The return
value is then used to do the actual sorting. Assuming that the basic type that is returned is a value-
type - i.e. a type that offers a "smaller-than" comparison based on its value - this allows us to sort the
list in a meaningful and consistent matter. Let's take a look at an example:
point a( 3 , 7 , 9 )
point b( 4 , 2 , 0 )
point c( 8 , 6 , 5 )
list<F3DPoint> l([a, b, c]) // content (and order) of l is [a, b, c]`
FCommand<double, F3DPoint> sortValue(F3DPoint.gety())
sortValue.set(F3DPoint.gety()) // store the command that returns the y-coordinate of a F3DPoint`
l.sort(sortValue) // use that command to get the value by which the list should be sorted
// the content (and order) of l is now [b, c, a]`
Wow! That might actually be useful, right? And it's not even as hard as you first thought (or as it
seemed based on may abstract explanation), is it? To reduce a little bit of typing, you can, of course,
pass the F3DPoint.gety() part directly to the creator of sortValue, or you can just pass it directly to the
sort-command!
l.sort(F3DPoint.gety()) // achieve the same thing as above!`
There's one thing I have to say about this last "shortcut", though, that I didn't manage to get our compiler to do. This code would also compile (and work) if you write the following:
l.sort(a.gety()) // will compile, but sorting has nothing to do with "a"`
Now, while this may actually seem convenient (less to type, right?), it may confuse the user into thinking that the point a is somehow involved in the sorting of the other points. I will show you a more confusing example of this problematic behavior very soon.
So, this first generic sort command for FList is based on the idea of "converting" an object of an
arbitrary type into a sortable value. What about the second one?
Item 16 - Predicate-based sorting of FList
The signature of it looks like this: FList<T>.sort(FCommand<FBool, T, T>). I call this the "predicate-based
sorting command". The FCommand that can be passed as an argument returns a boolean value, has a
receiver of the list's type, and takes one arguments of the same type. What does it mean?
Sorting is always done by determining a smaller-than relationship between two objects inside the list.
The first sort command does that be evaluating the smaller-than relationship of the (hopefully)
value-types that are returned by the FCommand it is given. This version of the sort command allows
to pass in the comparison-method itself which should return true if the first object is smaller and
false if it isn't (which would mean that it is greater or equal to the second one).
I guess an example may help here. I mentioned that you can use the FList<FDouble>.sort() command
(without arguments) to sort a list of doubles in ascending order. To achieve the same effect using the
version that takes an FCommand as a predicate, you could write:
FList<FDouble> l()
// fill the list`
FCommand<FBool, FDouble, FDouble> predicate()
predicate.set(FDouble < FDouble) // this will store the operator-command
// "<" (smaller) into the FCommand object "predicate"`
l.sort(predicate) // l will now be sorted in ascending order`
(Don't do that, it will definitely perform worse than just calling l.sort().)
Nice, so now it should be rather easy to come up with the code that sorts an FList<FDouble> in
descending order:
// assume we have a filled list<double> "l"`
FCommand<FBool, FDouble, FDouble> predicate(FDouble > FDouble)
// we now use the "larger than" operator for determining the "smaller than" relationship of two doubles`
l.sort(predicate) // l is now sorted in descending order`
That wasn't that hard, was it? Actually, it can be simplified even more because all commands can
implicitly be converted to their corresponding FCommand type. So the code above can be
abbreviated to
list<FDouble> l([ 2 , 1 , 9 , 6 , 5 ])
l.sort(FDouble > FDouble) // the list will contain [9, 6, 5, 2, 1]`
Now, there is something that you need to know about that implicit conversion. First of all, all implicit things have the drawback that they are, well, implicit. So, you do not always notice that they happen. For this particular case, you might think that the statement
auto cmd(FDouble < FDouble)
Will create a new FCommand object of type FCommand<FBool, FDouble, FDouble> , but it does not.
Unfortunately, it still compiles and you may be puzzled about the error messages the compiler will
throw at you when you try to use your " FCommand ". Do you remember the third limitation of the
auto - keyword? No implicit type conversions are possible and the command would need to be
implicitly converted to an FCommand. But why does this still compile? And what is the result? Ok, so
what the compiler sees here is that you are trying to declare a variable using auto and you are giving
it the comparison of two doubles (I know... you are actually giving it the comparison of types, but, as
crazy as it may sound, the type FDouble is an FDouble itself). The result of such a comparison is a
boolean value, so the variable cmd will actually be of type FBool! I know, that is not nice, but at the moment I do not see a way to change that. Due to the complexity an FCommand type may have, it
would be nice, though (just look at the get displacement-example above).
The second problem here is one that I already mentioned earlier. The following code will also compile and (though you might suspect otherwise) will behave just the same:
list<FDouble> l([ 2 , 1 , 9 , 6 , 5 ])
l.sort(FDouble < 10 ) // the list will contain [ 1 , 2 , 5 , 6 , 9 ]`
The command that is passed to the sort command is odd, isn't it? It looks like, all it is supposed to do
is to check whether the double value are smaller than the value "10". This should not change the
order of elements at all (all values are smaller than 10). Well, it doesn't behave like that. The
compiler will notice that you've passed in an object of a (internal) command type which is happily
converted into an FCommand object and it doesn't care that the argument of that smaller operator
has the value "10". Again, knowing this can actually save some typing, but to those users that didn't
read this document, this may behave differently from what they expected it to do. I am still unsure,
what to do: hack the compiler to catch this or leave the shortcut, so you write
list<FDouble> l([ 2 , 1 , 9 , 6 , 5 ])
l.sort( 1 < 1 ) // the list will contain [ 1 , 2 , 5 , 6 , 9 ]`
I'll leave this open for your vote, but I can already foresee what will be the outcome.
Item 17 - Sorting using functions
Fine. So we have seen, that the FCommand type actually has a meaningful purpose (apart from the
crazy things that you might be able to come up with - which will very likely force us to fix something
or implement new stuff).
However, there is something missing so far. If you don't feel the same, have a look at the examples above. Do you see the missing link? If not, I'll tell you: "we can sort the F3DPoints by their y- coordinate in ascending order and the doubles in descending order, but how can we sort our F3DPoints by their y-coordinate in descending order?"
Recap: The first version of sort, that "converts" any object to a "sortable" type will always sort in
ascending order. The second one can only be used if a command exists that can be used on the type
we are interested in, takes exactly one argument of the same type and returns the type FBool (just as
the boolean operators do). Now, unfortunately, this is limited to a very small set of types.
So, what can we do, in order to sort our FList<F3DPoint> by y-value in descending order? Yes, we can
go back to implementing the sorting inside the feature itself or sort in ascending order using the code
above and then call reverse. Do we really want to do that? (If I ask like that, the answer has to,
obviously, be "No", I guess you've learned that by now.)
Instead, we can use a function that we can freely implement in our feature and this is the point
where the third generic version of the FList<T>.sort command comes into play. That third version
takes an FCommand that stores a global command which takes two arguments and returns a bool.
Conveniently, when inside the Feature that implements the function, functions are treated just like
global commands.
How does this work? In a sense this sort command is actually pretty similar to the second sort
command we've looked at, because it takes a “predicate- FCommand ”, i.e. one that determines
whether one value is "smaller" than the other (since that is what is used for sorting) and returns true
or false based on that.
So, the third version of the FList.sort() command has the following signature:
FList<T>.sort(FCommand<FBool, FNull, T, T>).
Now, there is no global command, that I can think of, that would suit that signature for any given type (i.e. take two arguments of the same type and return a bool), but if you happen to stumble upon such a command, you could use it for sorting your list. If you happen to do so, you can really shine when telling this a fellow feature-buff at our next user's meeting, I suppose. Anyways, jokes aside, the relevant part is that this sort command allows utilizing your very own function to do the sorting. So, here's the implementation of a feature that actually sorts a list of points descending by the points' y-coordinate:
function predicate(F3DPoint lhs, F3DPoint rhs) : FBool
return (lhs.gety() > rhs.gety())
endfunction
FList<F3DPoint> l()
// fill the list`
// capture the sorting function into an FCommand object`
FCommand<FBool, FNull, F3DPoint, F3DPoint> cmd(predicate(F3DPoint, F3DPoint))
// now sort`
l.sort(cmd)
This actually works! Yes, I agree, it does take some typing to spell out the type of the FCommand
object, but it still saves many keystrokes compared to implementing the sorting algorithm inside the
feature, not to mention the better performance. Also, it is possible to reduce the typing to
l.sort(predicate(F3DPoint, F3DPoint))
To be honest, though, so far I didn't do a performance comparison between this version of sort and the combination of the first version followed by a call to reverse, yet, but I am rather confident that the version that is using the function will take the cake.
For completeness, there is a fourth FList.sort command that takes an FCommand Object. Just like
the one we just talked about, that FCommand captures a global command (which, as we just saw,
includes functions). However, as opposed to the previous one, that global command takes only one
parameter of the list's type and returns a value of type FBasicType, which is then used for the actual
sorting. So, basically, it is the mixture of the first and the third sort-command I talked about. Using
that, it would also be possible to sort our list of points in descending order based on their y-values:
function negativeY(F3DPoint p) : FDouble
return (-p.gety())
endfunction
FList<F3DPoint> l()
// fill the list`
FCommand<FDouble, FNull, F3DPoint> cmd(negativeY(F3DPoint))
l.sort(cmd)
Once more: Interlude
Ok, this concludes the chapters about the sort commands that are offered by FList<T>. However,
they are not the only commands that take a generic FCommand as an argument. Flist<T> also offers a
removeIf(FCommand<FBool, T>) command (and an overload that allows to use a global command, i.e. a function) which removes an element from the list if the FCommand returns "true". I'll just skip that
one, since I assume that you can deduce it from the previous items.
Item 18 – Searching in an FList
Wow, another item talking about FList and its commands? "Do you want to make FList the one-stop-
omnipotent-container-type in CAESES?" Well, to be honest, I actually do (if there is no need for
associative containers)! So, let's look at some more stuff FList has to offer:
Another set of commands offered by FList that accept FCommand s as parameters are several options
to find certain elements in the list and those commands re-use two wonderful type we have already
learned about: the FIteratorPair<T, U> and FCommand<T, U, ...,>.
Now, while you may deduce the use of FCommand for this from the previous item about sorting, you
may ask "why FIteratorPair? FList only has one template argument, what's the second one?" So, the
type that is actually used is FIteratorPair<FInteger, T>. So, those find commands do not only return
the object that is stored in the list (second part of the pair), but also return its index (first part of the
pair). Additionally, they offer the isValid command on the returned pair that indicates whether the
value was found at all (actually, if it wasn't the index, i.e. the first part, is set to -1).
The commands that offer that functionality are find, minElement, and maxElement. Let's step through
them.
FIteratorPair FList<T>.findIf(FCommand<FBool, T>, FUnsigned from= 0 , FInteger to=- 1 )
This command will return the first occurrence of an object where the given command, executed on that object returns true. The command that should be executed can take no parameters and must return something that is convertible to bool. Example:
list<fcurve> curveList()
// fill list with curves`
auto it(curveList.findIf(fcurve.isvisible()))
bool exists(it.isvalid())
This code will try to find the first curve that is stored in the list that is visible. If no such curve exists, the variable exists will hold the value false, otherwise it will be true.
As you can see the command takes two more arguments which allows to specify a range in which to look in. This way you can, for example iterate over all visible curves in our list:
FList<fcurve> curveList()
// fill list with curves`
FCommand<bool, fcurve> cmd(fdrawable.isvisible())
auto it(curvelist.findIf(cmd))
while (it.isvalid())
// do something with the visible curve`
auto it(curvelist.findIf(cmd, it.first() + 1 )) // search the next visible curve`
endwhile
This time, I made a loop that looks for the next visible curve in the list after processing the first one. Of course, I have to start searching after the index of the one that was found before, hence the "+1".
Did you notice that I stored the fcurve.isvisible() command into a variable of type FCommand this
time? Why would I do that? It's definitely more typing (well, is it? I am not sure and too lazy to count). The reason is not to save typing but to get better performance. Remember Item 4 of Effective FPL? Use temporary variables! Instead of the code above, we could have written.
while (it.isvalid())
// do something with the visible curve`
auto it(curvelist.findIf(fcurve.isVisible(), it.first() + 1 )) // search the next visible curve`
endwhile
But what would have happened then? A new FCommand object would have been created for each
iteration including the overhead of the implicit conversion from the internal command-type to the
user-accessible FCommand. It's not much, but it's something that we've saved. So you see, even with
those new fancy types, the contents of Effective FPL stays valid.
In most cases the next version of find will be much more interesting, though:
FIteratorPair FList<t>.findIf(FCommand<FBool, FNull, T>, FUnsigned from= 0 , FInteger to=- 1 )
This version takes a global command that defines a predicate that needs to be fulfilled by the object in the list. Remember that functions that are defined in the feature are treated as global commands. So, this version of findIf allows you to find a lot more than the first one. You could look for all curves in the curve list that have a length that is smaller than a particular value, for example. You can pretty much search for whatever you like (as long as you can program it as a function that takes an element of the list as an argument and returns true or false).
Actually, since such code includes some pitfalls even for experienced programmers, let's take a look at code that uses this findIf command in order to remove elements from a list that satisfy a certain predicate (you could use removeIf, which would probably yield better performance, but there are always people who want to reinvent the wheel, so let's pretend we belong to that species).
Wrong code ahead, do not use!
Let's remove all invisible curves from our list of curves:
function isInvisible(fcurve c) : FBool
return (!c.isVisible())
endfunction
list<fcurve> curveList()
// fill list with curves`
// store function for good prerformance (see above)`
FCommand<bool, FNull, fcurve> cmd(isInvisible(fcurve))
auto it(curvelist.findIf(cmd)) // find first invisble curve`
while (it.isValid())
curvelist.removeAt(it.first()) // remove the invisible curve from the list`
auto it(curvelist.findIf(cmd, it.first() + 1 )) // search the next invisible curve`
endwhile
Now, can you spot the bug in that code? What happens after you removed an entry of the list? Right, all following entries move up. So the + 1 part in the findIf-call has just become a bug! The algorithm will actually skip an entry in the list which might be an invisible curve, just the type of curves you wanted to get rid of!
So, just for completeness, the correct code would be:
function isInvisible(fcurve c) : FBool
return (!c.isVisible())
endfunction
list<fcurve> curveList()
// fill list with curves`
// store function for good prerformance (see above)`
FCommand<bool, FNull, fcurve> cmd(isInvisible(fcurve))
auto it(curvelist.findIf(cmd)) // find first invisble curve`
while (it.isValid())
curvelist.removeAt(it.first()) // remove the invisible curve from the list`
auto it(curvelist.findIf(cmd, it.first())) // search the next invisible curve – NO +1!`
endwhile
I'm just stressing this because it's the dreaded "off-by-one"-error every programmer goes through at least once in their career (this "once" has been nominated to be the understatement of the year, by the way – seriously, it happens to me a lot and I usually already know that it's the case, but then I am unsure about it and fire up the code, that I already know is buggy, in the debugger... this happens especially when working with strings, my arch-enemy!).
There are two more commands (and overloads) that search for elements in a list: maxElement and minElement. As the names suggest, they search for the element with the maximum value or the minimum value. Now, for this to work, we have to have some type of “smaller-than“ comparison. Remember FMap? Well, it works quite similar: for a list that contains basictype-objects these commands can be executed without additional arguments. The comparison will be based on the objects' values (if it is a value-type, just like the sort-criterion of the FMap ). For other types, it is possible to specify an FCommand that “converts“ the object to a comparable type. This way, for example, it is possible to find the point with the largest x-coordinate using the following code:
list<F3DPoint> pointList()
// fill list with points`
auto it(pointList.maxElement(F3DPoint.getx())) // find point with maximum x-value`
Just like the find commands, it is possible to specify a range of the list to search in.
Epilogue
Wow, it's been an exciting ride, don't you agree? This surely wasn't as straight forward as the stuff in Effective FPL , right?
Well, I hope that at least some of you have made it this far (yes, you can already have a cookie, you've really deserved it!). I sincerely hope that I did not scare you off with all that mumbo-jumbo.
I, personally, think that the generic types CAESES provides help you Feature programmers to implement complex algorithms more easily. It was a lot of fun for me to implement all of this and it took a lot less time than I initially thought.
Of course, I am aware (and you should be, too) that such a large new feature will not be perfect from the start. I hope that, by the time you read this, the big problems will already have been found by our internal testers and, then, be fixed by me (or one of the other developers). If you do happen to find problems, bugs, or just missing stuff in the template-implementation (or anything else in CAESES for that matter), please do not hesitate to voice it using the helpdesk or the forum.
What is left for me to say is
- a) I am really looking forward to the crazy Features that make use of the new capabilities,
- b) I really hope that I was able to transport this – admittedly – complicated topic to you in an understandable way, and
- c) you've now earned your cookie of the day - well actually this time you've earned a beer (or lemonade if you prefer)!
So, cheers! Thanks for reading.