Refactor number types

Issue #588 closed
Pol Welter created an issue

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 have HNumber. The latter is also responsible for the units, so that currently each CNumber 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. Then CNumber is a complex number (still without unit) made up of two HNumbers. Finally, we implement the unit stuff in a new type Quantity, 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)

  1. Hadrien Theveneau

    @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.

  2. Pol Welter 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 each Value. When set (or non empty) it overwrites the formatter settings. I don't see the need of an extra type FormattedQuantity 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,...)...

  3. Pol Welter 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?

  4. Pol Welter 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 in CMath or HMath. 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 to functions.cpp. All that can be seen from there is a Value type, which stores a Quantity, storing in turn a CNumber, ...

    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 where CNumber 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

  5. Hadrien Theveneau

    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 and as_XXX(), where XXX is the type, which which would return the value in the type needed. A more practical approach would to have is_CNumber()and is_HNumber() functions as well as as_CNumber() and as_HNumber() functions.

    I would suggest to keep HNumber and CNumber classes in addition to add the Quantity and Value classes. Yes, this will be a lot of classes, but each class manages its stuff. HNumber would always be a real number and CNumber 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,

  6. Pol Welter 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 and CNumber classes in addition to add the Quantity and Value classes. Yes, this will be a lot of classes, but each class manages its stuff. HNumber would always be a real number and CNumber 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 for Value is Quantity. The latter implements all the necessary functions for dimensional math (which I put into a class named DMath: I did not want it to sound like it was part of Qt). Also I have removed all the unit & dimension related stuff from HNumber and CNumber. Both now only represent numbers. So Quantity stores two CNumbers (one for numeric value and one for unit). In turn, CNumber stores two HNumbers. 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 an HNumber!) to represent the numerical value and unit. For the same reason as for CNumber, I would prefer if Quantity 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 inside Quantity 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 be functions.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 (like Quantity<CNumber, CMath>)... Then it would be up to Value to spawn the correct type.

  7. Hadrien Theveneau

    @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.

  8. Pol Welter reporter

    So you're suggesting I manually implement RealQuantity and ComplexQuantity?

    My point is not that Quantity should rely on HNumber rather than CNumber, I just want a way to have different functions that handle complex or real mode.

  9. Hadrien Theveneau

    No. I'm suggesting to implement only ComplexQuantity. HMath will handle HNumber, CMath will handle CNumber. For quantities, I must think a bit more.

  10. Pol Welter reporter

    I don't especially like that idea for the reason that it will require each function to be overloaded again twice (in Hmath and CMath). This does not save us any work compared to creating a new class RealQuantity, but hierarchically speaking, these functions do not belong into HMath or CMath.

    I have another idea, will get back once I tried it out...

  11. Hadrien Theveneau

    The more I think, the more I dislike my own idea either. I am waiting for your other idea. Kind regards, Hadrien

  12. Felix Krull

    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 and CMath inherit from a common "math-function-interface" class (it may even be possible to make CMath a subclass of HMath such that it would only have to reimplement the functions that are particularly different).
    • So the common interface is actually relevant, HMath and CMath get a global static or singleton instance or so.
    • DMath would have a settable pointer to the math object to use (so DMath 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 and ComplexMath rather than associating them with the number types like that.

    Alternatively, separate RealDMath and ComplexDMath classes. These might be template-able to a degree from a single DMath<NumberType, MathClass> template. Come to think of it, that's pretty much the same pattern as above but at compile-time rather than runtime.

  13. Pol Welter 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 and ComplexDMath classes. These might be template-able to a degree from a single DMath<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.

  14. Pol Welter 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 the Quantity 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...

  15. Pol Welter 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.

  16. Tey'

    "Resolved" generally means the issue has been solved but this needs confirmation/review. "Closed" is more definitive.

  17. Log in to comment