views:

113

answers:

3

I have a Money Type that allows math operations and is sensitive to exchange rates so it will reduce one currency to another if rate is available to calculate in a given currency, rounds by various methods. It has other features that are sensitive to money, but I need to ask if the basic data type used should be made generic in nature.

I've realized that the basic data type to hold an amount may differ for financial situations, for example:

  • retail money might be expressed as all cents using int or long where fractions of cents do not matter,
  • decimal is commonly used for its fixed behaviour,
  • sometimes double is used for finance and large values
  • sometimes a special BigInteger or 3rd-party type is used.

I want to know if it would be considered good form to turn Money into Money<TAmount> and where TAmount: struct so it can be used in any one of the above chosen scenarios?
TAmount would represent the basic amount value and be any of the types mentioned above or others.

+1  A: 

It would be a good idea, except that there isn't any good way to constrain the TAmount. (at compile-time)

Since .Net doesn't have a INumeric interface, all you can use is where TAmount : struct.

In addition, there is no simple way to perform arithmetic on a generic type parameter.
However, you can use Jon Skeet's MiscUtil library.

SLaks
There are so many times I wished .NET had an `INumeric` interface.
ChaosPandion
Why was this downvoted?
SLaks
As far as granularity of `where` and absence of `INumeric`, I can do some restraining checks for "allowed" types although it's slightly redundant to the cause: `if (TAmount is int || TAmount is decimal || TAmount is long)...` Or I can make that check an external or injected dependency. It's doable I think.
John K
As far arithmetic, I could resort to abstract methods ala Java style like: `abstract Money<T> Plus<T>()...` and an abstract base class `BaseMoney<T> where T:struct` so to implement a new money the developer would have to derive and then overload maths.
John K
@JDK#1: But you can't make `Money<DateTime>` a compile-time error.
SLaks
@SLaks: I'd go for runtime error, unless there's a huge backlash. I mean, if somebody is using some weird combo of Money<string> or Money<Random> then they DESERVE runtime blowout.
John K
@JDK: I'm not saying you shouldn't do that - I'm saying that it's not perfect.
SLaks
For arithmetic, I've before suggested C# get the ability to constrain generics to operators, so you could declare a method like: `T Sum(IEnumerable<T> source) where T : operator(T=T+T)`, instead of creating a gazillion overloads.
JulianR
@JulianR: Interesting proposal. Does it exist in other languages?
John K
@JDK: Yes; C++, for example. (Implicitly)
SLaks
The MiscUtil library is useful. It's by both Jon Skeet http://stackoverflow.com/users/22656/jon-skeet and Marc Gravell http://stackoverflow.com/users/23354/marc-gravell as the first sentence Notes.
John K
@jdk - F# supports arbitrary member constraints, including operator constraints.
kvb
+2  A: 

The problem with trying to genericise the type of the payload is that:

  1. you lose performance in the current implementation
  2. Attempts to mitigate the performance hit impact readability1
  3. the (safe) conversion rules for the payload type designed to never lose precision may not be appropriate to your code which will be more concerned with retaining numerical exactness for example.
  4. You gain very little compile time value from it except when attempting to assign differing payload types to each other.

Instead what would be useful is being able to check when conceptually different values are used together, like USD and GBP for example. You might like to take a look at the alternate method used in recent f# releases called Units of Measure.

For more details have a read of the designer's blog but note that the code samples are from earlier releasaes so are somewhat out of date.

Luca (previously on the f# team, now working in finance) has made use of this for example

The excellent CTO Corner f# book details this in chapter 3


  1. I should note the code in MiscUtils is really quite excellent, it just can't compete with '+'
ShuggyCoUk
+2  A: 

I don't think it should be generic. I think it could end producing non-intuitive results.

If you allow people to use any generic type they want to hold the "value" then you will invariably end up with many different instances using different value types. So what happens when you have a Money<decimal>, but some other part of the application is asking for a Money<uint>? What if somebody forgets to clearly document whether that part of the API is expecting the currency in dollars or cents? How do I convert from one to the other?

And what about the opposite case, where I have two Money<ulong> values that appear to be equal, except it turns out that one is in dollars and one is in cents? All of the arithmetic operations appear to be valid, but they actually aren't. What we technically have (or should have) are two different currencies, not just two different numeric representations of the same currency.

Then of course there are the obvious arguments about illegal value types; what would a Money<DateTime> or a Money<Nullable<Guid>> represent? How would one even code the arithmetic operations, not knowing what operators are available on the generic type parameter?

Even working within types we have problems. The whole idea behind a Money type is that is supposed to abstract the math and the conversion. But what happens when we need to convert a Money<int> in USD to a Money<int> in CAD? What happens to the decimal points? Do we just throw them away? Should it raise an exception? Should it produce some sort of compiler warning about a loss of precision (and if so, how)?

I wouldn't do it. It just raises too many complicated questions about the type's semantics. Better that Money is just one type that is actually optimized for working with currencies.

Aaronaught
All very good questions. I wouldn't allow different types e.g. Money<uint> and Money<int> to be mixed. However everything else is food for thought.
John K