- changed title to Refactor number types
Refactor number types
Here's a discussion on the refactor of the number types. We had it originally on Hadrien's repo, but I thought it might now be a good idea move it over here. This way, we won't lose track of it in the future.
Here's my summary of the most important points made.
Pol Welter wrote:
Apart from the new
CNumber
we still haveHNumber
. The latter is also responsible for the units, so that currently eachCNumber
effectively stores its unit twice. We had this discussion already, and concluded that for the moment, this was acceptable, but needs to be tackled later. How shall we deal with it in precisely?I suggest the following:
Remove all the unit related stuff from
HNumber
, so that it really only represents a real number. ThenCNumber
is a complex number (still without unit) made up of twoHNumbers
. Finally, we implement the unit stuff in a new typeQuantity
, which represents - you guessed it - a physical quantity with its dimension and unit. It is this class that is used by the evaluator.Also please come up with a better name than 'Quantity'.
Felix Krull wrote:
Without dwelling on this subject too much right now, I think that if we refactor the number stuff, it'd be worthwhile to pull in another abstraction layer so that the evaluator can potentially handle values that aren't actually numbers. I was particularly thinking of https://bitbucket.org/heldercorreia/speedcrunch/issues/508/ascii-value-and-conversions, which'd require the evaluator to parse and handle strings. Without making any arguments on whether supporting ASCII conversion is a good idea (I mean, I'd use it, but it's a bit out of place for a calculator), I'm thinking of making the evaluator handle a
Value
class roughly like this:
class Value {
enum Type {
Quantity,
// This would just be a Quantity and a format identifier so that stuff could be moved out of the number classes
FormattedQuantity,
// other hypothetical things like strings
};
union {
...
}
// For anything that's strictly a number, would return the value for the purposes of arithmetic. For
// anything else, would return NaN.
Quantity toQuantity();
// Format for display purposes.
QString toString();
// etc.
};
The idea here being that the evaluator could at least parse and pass around non-number values. All arithmetic and most built-in functions would still be limited to working with numbers, but functions could take non-number arguments and return non-number values. Even if we don't define any non-number value types, I think this would be a good abstraction to have.
Hadrien Thevenau wrote:
One idea: why should the different types be exclusive (like in a union) ? A single "thing" can have several types of value. For example, 1.0 can have both the 1.0 value and "1.0" value. Additionally, a variable has both an l-value (the place in memory in which information is stored) and a r-value (the actual value).
It would also allows to simplify the current parser with units. In the current design, there is a post-processing step after the lexing and before the parsing to process the unit conversion operator in order to feed it the unit display string. Allowing a value to have both a string and a number value would solve this problem easily: the number value would be used for the numeric and the display value for the display.
Felix Krull wrote:
So associating several representations with a single value? So the unit conversion operator would parse its right-hand argument into a value that contains both the actual string that was entered -- for display purposes -- and the associated numerical value for math purposes? That seems like a sensible solution for that situation, but I'm not sure that needs to be generalised into having any arbitrary number of representations for each value or something. I'm still in favour of just a QVariant-style smart union. I don't think there's a wide variety of situations where the representation of a number is not just that number, outside of hex() and friends and the unit conversion operator.
The discussion then veered off topic, and we discussed a refactor of the parser instead. Will open another issue on that matter.
Comments (25)
-
reporter -
@fk: I thank more about this topic, and I propose and intermediate solution mixing our points of view. For the value, only allow one single type, in a QVariant fashion, but for the token, in addition to the value, store also the corresponding string pointers.
-
reporter Sounds reasonable.
About the format stored in the actual type: I think doing it similar to the current way doesn't sound too bad. Keep a
char
(or possibly a string; gives us a bit more headroom) within eachValue
. When set (or non empty) it overwrites the formatter settings. I don't see the need of an extra typeFormattedQuantity
for this. (I might be missing something; if so, please enlighten me :)Why could we use a string instead of a char? I imagine complex numbers output in either polar or rectangular form. This setting is independent on the numeric base that we chose. Which in turn should ideally be independent on how we wish to display scientific notation (engeneering, fixed,...)...
-
reporter - edited description
-
reporter I have started with this. God, this is SOOO necessary... What was I thinking when writing this code...
Anyway, this refactor will once again break the history files. I guess this is ok?
-
repo owner That's fine since we haven't released yet.
-
reporter I made some progress, but ran into trouble. In fact, currently the Enable Complex numbers switch takes effect in
functions.cpp
. According to the setting, it either calls the functions inCMath
orHMath
. The problem is, if we introduce one or two more layers of abstraction, then the complex or real nature of the underlying type is no longer accessible tofunctions.cpp
. All that can be seen from there is aValue
type, which stores aQuantity
, storing in turn aCNumber
, ...One way would be to move the settings switch into the math types, i.e. that
CNumber
checks the settings, and takes action accordingly. However I really liked Hadrien's approach whereCNumber
is always a complex type, regardless of any settings.Any better ideas? I need some advice from you guys on this design decision. @fk, @teyut, @heldercorreia, @thadrien
-
Hi Pol,
I would advice against moving processing settings into
CNumber
, because they are not used by the number itself but by the processing engine.I think the abstraction shouldn't prevent to know the underlying type. An integrist approach would be to have a function
get_type()
which would return the underlying type andas_XXX()
, whereXXX
is the type, which which would return the value in the type needed. A more practical approach would to haveis_CNumber()
andis_HNumber()
functions as well asas_CNumber()
andas_HNumber()
functions.I would suggest to keep
HNumber
andCNumber
classes in addition to add theQuantity
andValue
classes. Yes, this will be a lot of classes, but each class manages its stuff.HNumber
would always be a real number andCNumber
always a complex one.For math functions,
HMath
would contain only real functions,CMath
only complex functions. If needed, we can add other math packets. For example,QMath
for quantity maths, and so on. But only if needed.Have a nice day, Best regards,
-
reporter I would advice against moving processing settings into
CNumber
, because they are not used by the number itself but by the processing engine.My thoughts exactly.
I would suggest to keep
HNumber
andCNumber
classes in addition to add theQuantity
andValue
classes. Yes, this will be a lot of classes, but each class manages its stuff.HNumber
would always be a real number andCNumber
always a complex one.Already in place. This is not the problem.
I think I should have elaborated more on what I have done so far. I have implemented
Value
, pretty much according to what Felix outlined. Currently the only valid type forValue
isQuantity
. The latter implements all the necessary functions for dimensional math (which I put into a class namedDMath
: I did not want it to sound like it was part of Qt). Also I have removed all the unit & dimension related stuff fromHNumber
andCNumber
. Both now only represent numbers. SoQuantity
stores twoCNumber
s (one for numeric value and one for unit). In turn,CNumber
stores twoHNumber
s. Essentially like this:class HNumber { // stuff } class HMath { // Math functions for HNumber } class CNumber { HNumber real; HNumber imag; } class CMath { // Math functions for CNumber } class Quantity { CNumber numericValue; CNumber unit; } class DMath { // Math functions for Quantity }
My problem is that Quantity stores a
CNumber
(not anHNumber
!) to represent the numerical value and unit. For the same reason as forCNumber
, I would prefer ifQuantity
was agnostic on the complex numbers setting. So how can we implement this switch? Seen from outside,Quantity
is black box, we should not have access to the inner workings of it. We don't want the switch insideQuantity
either, for the outlined reason above.I hope you see my dilemma more clearly now. I could get by, by indeed implementing the switch into
Quantity
, but this does not feel satisfactory. I feel like it should befunctions.cpp
's job to take care of this, but I don't see how to do so...One solution would be to implement two
Quantity
classes, one real, and one complex. That will result in yet another set of functions to be maintained. Maybe we could achieve this with a template (likeQuantity<CNumber, CMath>
)... Then it would be up toValue
to spawn the correct type. -
reporter Update: I don't manage templating of
Quantity
. I am by no means an expert with the template mechanism, but it seems that the templated operators must be defined inline (i.e. in the header). For it to work, I'd have to rewrite all of them asfriend
s. I am not feeling it... -
@polwel: It IS important that we can switch to real-mode calculation, because some important functions behave differently in the two modes, particularly trigonometric. However, this does NOT prevent us to use
CNumber
for all numbers, even real ones. Yes, it will waste space, but I think it is not worth the trouble with templates.So, you're not the only one who doesn't feel it.
I like very much the idea of templates but I think the C++ implementations (yes, plural is needed here) are troublesome.
-
reporter So you're suggesting I manually implement
RealQuantity
andComplexQuantity
?My point is not that
Quantity
should rely onHNumber
rather thanCNumber
, I just want a way to have different functions that handle complex or real mode. -
No. I'm suggesting to implement only
ComplexQuantity
.HMath
will handleHNumber
,CMath
will handleCNumber
. For quantities, I must think a bit more. -
reporter I don't especially like that idea for the reason that it will require each function to be overloaded again twice (in
Hmath
andCMath
). This does not save us any work compared to creating a new classRealQuantity
, but hierarchically speaking, these functions do not belong intoHMath
orCMath
.I have another idea, will get back once I tried it out...
-
The more I think, the more I dislike my own idea either. I am waiting for your other idea. Kind regards, Hadrien
-
Having different
Quantity
classes for complex and real seems unmanageable. If the number type changes between real and complex mode, that seems to imply we would have to basically recreate every number object in e.g. user variables when the user switches modes.I'm not sure if the
DMath
functions are always agnostic to whether they're doing complex or real maths under the hood? In any case, some ideas which hopefully aren't entirely missing the point. Approach a:HMath
andCMath
inherit from a common "math-function-interface" class (it may even be possible to makeCMath
a subclass ofHMath
such that it would only have to reimplement the functions that are particularly different).- So the common interface is actually relevant,
HMath
andCMath
get a global static or singleton instance or so. DMath
would have a settable pointer to the math object to use (soDMath
should probably also have an instance).- Switching modes would then run some code like this:
DMath::instance()->setMathBackend(CMath::instance());
This could also be handled by having separate instances of DMath for complex and real calculations and picking on or the other in functions.cpp, if we don't want that kind of global state in the math system. The downside is that HMath and CMath would have to use a common number type, so HMath would have to work on a type that sits above it in the hierarchy. Maybe play naming shenanigans, call the classes
RealMath
andComplexMath
rather than associating them with the number types like that.Alternatively, separate
RealDMath
andComplexDMath
classes. These might be template-able to a degree from a singleDMath<NumberType, MathClass>
template. Come to think of it, that's pretty much the same pattern as above but at compile-time rather than runtime. -
reporter If the number type changes between real and complex mode, that seems to imply we would have to basically recreate every number object in e.g. user variables when the user switches modes.
Good point, had not thought about that one. That pretty much rules out separate classes indeed.
The downside is that HMath and CMath would have to use a common number type.
Not if we put all the casting into the
DMath
class(es).Alternatively, separate
RealDMath
andComplexDMath
classes. These might be template-able to a degree from a singleDMath<NumberType, MathClass>
template. Come to think of it, that's pretty much the same pattern as above but at compile-time rather than runtime.Nice thinking, that sounds like the best idea so far. Will see what I can do...
Btw, my 'idea' consisted in getting around the templating limitations by excessive use of macros. Not pretty. Will trash that one.
-
reporter Ok, with the input from Felix, here's my suggestion. I think it will be by far the easiest one to implement.
DMath
gets the switch:static bool complexMode = true;
Unlike querying
Settings
this will still ensure that theQuantity
class could work independently on the actual app itself. When switching the setting, we just need to remember the above variable as well. Seems fair enough!Weird how I didn't think of this earlier...
-
reporter Great, the bulk of the work has been done. I am sure there are still plenty of bugs that need fixing, but AFAICT, everything should be in place. Much nicer than before. Alos, we now have the framework required for properly adding units to the testsuite.
If you'd like to take a look, head over to my repo.
-
reporter - changed status to resolved
-
repo owner - changed milestone to 0.12
-
assigned issue to
-
repo owner - changed status to closed
See pull request #45
-
reporter Why closed rather than resolved? Just wondering.
-
"Resolved" generally means the issue has been solved but this needs confirmation/review. "Closed" is more definitive.
-
reporter Thanks for the clarification.
- Log in to comment