Money monoid
Mark Seeman talks about functional programming quickly and easily. To do this, he began to write a series of articles on the relationship between design patterns and category theory . Any OOPshnik who has 15 minutes of free time will be able to get their hands on a fundamentally new set of ideas and insights regarding not only functionalism, but also the correct object-oriented design. The decisive factor is that all the examples are real C #, F #, and Haskell code .
This hubrapost is the second article from a series of articles about monoids:
- Monoids, semigroups and all-all-all
- Money monoid
- Convex hull monoid
- Tuple monoids
- Function monoids
- Endomorphism monoid
- Monoids accumulate
Before starting, I would like to make a small digression on the title of the article. In 2003, Kent Beck's book, Extreme Programming: Development through Testing , which is originally called Test-Driven Development by example , was published, which has already become a bestseller . One such “example” was “Money example” - an example of writing and refactoring an application that can perform multicurrency operations, such as adding 10 dollars and 10 francs. The title of this article is a reference to this book, and I strongly recommend that you familiarize yourself with its first part in order to better understand what the article is about.
The "Money example" Kent Beck has some interesting properties.
In short, a monoid is an associative binary operation that has a neutral element (sometimes also called unity ).
In the first part of his book, Kent Beck explores the possibility of creating a simple and flexible "money API" using the principle of "development through testing." As a result, he gets a solution, the design of which requires further elaboration.
Kent Beck API
В этой статье используется код из книги Кента Бека, переведенный Яваром Амином на язык С# (оригинальный код был написан на Java), который я форкнул и дополнил.
Кент Бек в своей книге занимался разработкой объектно-ориентированного API, способного обрабатывать деньги в нескольких валютах, с возможностью работы с выражениями (expressions), такими как «5 USD + 10 CHF». К концу первой части он создает интерфейс, который (переведенный на язык C#), выглядит так:
public interface IExpression
{
Money Reduce(Bank bank, string to);
IExpression Plus(IExpression addend);
IExpression Times(int multiplier);
}
The method
Reduce
converts the object IExpression
to a certain currency (parameter to
), represented as an object Money
. This is useful if you have an expression that has multiple currencies. The method
Plus
adds the object IExpression
to the current object IExpression
and returns a new one IExpression
. It can be money in one currency or in several. The method
Times
multiplies IExpression
by a specific factor. You probably noticed that in all the examples we use integers for the factor and the sum. I think Kent Beck did this in order not to complicate the code, but in real life when working with money we would use fractional numbers (for example, decimal
). The expression metaphorlies in the fact that we can simulate working with money, like working with mathematical expressions. A simple expression will look like 5 USD , but it can also be 5 USD + 10 CHF or 5 USD + 10 CHF + 10 USD . Although you can easily reduce some simple expressions, such as 5 CHF + 7 CHF , you cannot calculate the expression 5 USD + 10 CHF if you do not have an exchange rate. Instead of trying to immediately calculate the money transactions, in this project we create an expression tree, and only then we transform it. Sounds familiar, right?
Kent Beck in his examples implements the interface in
IExpression
two classes:Money
represents a certain amount of money in a specific currency. It contains the “Amount” (quantity) and “Currency” (currency name) properties. This - the key point:Money
is the value objects ( of value object ).Sum
is the sum of two other objectsIExpression
. It contains two terms, called Augend (the first term) and Addend (the second term).
If we want to describe the expression 5 USD + 10 CHF , it will look something like this:
IExpression sum = new Sum(Money.Dollar(5), Money.Franc(10));
where
Money.Dollar
and Money.Franc
are two static factory methods that return objects Money
.Associativity
Have you noticed that
Plus
this is a binary operation? Can we consider her a monoid? To be a monoid, it must satisfy the laws of the monoid , the first of which states that the operation must be associative. This means that for the three facilities
IExpression
, x
, y
and z
, the expression x.Plus(y).Plus(z)
must be equal x.Plus(y.Plus(z))
. How should we understand equality here? The return value of a method Plus
is an interface IExpression
, and interfaces do not have such a thing as equality. Therefore, either equality depends on specific implementations ( Money
and Sum
), where we can determine the appropriate methods, or we can use test correspondence (testing pattern,test-specific equality , - approx. per. ) The xUnit.net testing library supports test compliance through the implementation of custom comparators (for a detailed study of unit testing capabilities, the author suggests taking his Advanced Unit Testing course at Pluralsight.com). However, the original Money API already has the ability to compare objects of type
IExpression
! The method
Reduce
can convert any IExpression
into an object of type Money
(that is, to a single currency), and since it Money
is an object-value, it has structural equality (more about value objects and their features can be found, for example, here) And we can use this property to compare objects IExpression
. All we need is an exchange rate. Kent Beck uses the 2: 1 exchange rate between CHF and USD in his book. At the time of this writing, the exchange rate was 0.96 Swiss francs to the dollar, but since the example code everywhere uses integers for money transactions, I will have to round the rate to 1: 1. This, however, is a rather stupid example, so instead I will stick to the original 2: 1 exchange rate.
Now let's write the adapter between
Reduce
and xUnit.net as a class :IEqualityComparer
public class ExpressionEqualityComparer : IEqualityComparer
{
private readonly Bank bank;
public ExpressionEqualityComparer()
{
bank = new Bank();
bank.AddRate("CHF", "USD", 2);
}
public bool Equals(IExpression x, IExpression y)
{
var xm = bank.Reduce(x, "USD");
var ym = bank.Reduce(y, "USD");
return object.Equals(xm, ym);
}
public int GetHashCode(IExpression obj)
{
return bank.Reduce(obj, "USD").GetHashCode();
}
}
You have noticed that the comparator uses an object
Bank
with a 2: 1 exchange rate. A class Bank
is another object from Kent Beck's code. It itself does not implement any interface, but is used as an argument to a method Reduce
. To make our test code more readable, we add an auxiliary static class:
public static class Compare
{
public static ExpressionEqualityComparer UsingBank =
new ExpressionEqualityComparer();
}
This will allow us to write an assert that checks equality for the associativity operation:
Assert.Equal(
x.Plus(y).Plus(z),
x.Plus(y.Plus(z)),
Compare.UsingBank);
In my fork of the Java Amin code, I added this assert to the FsCheck test, and it is used for all objects
Sum
and Money
that FsCheck generates. In the current implementation
IExpression.Plus
, it is associative, but it is worth noting that this behavior is not guaranteed, and here's why: it IExpression
is an interface, so someone can easily add a third implementation that will violate associativity. Conventionally, we will assume that the operation is Plus
associative, but the situation is delicate.Neutral element
If we accept that it is
IExpression.Plus
associative, then this is a candidate for monoids. If there is a neutral element, then it is definitely a monoid. Kent Beck did not add a neutral element to his examples, so add it yourself:
public static class Plus
{
public readonly static IExpression Identity = new PlusIdentity();
private class PlusIdentity : IExpression
{
public IExpression Plus(IExpression addend)
{
return addend;
}
public Money Reduce(Bank bank, string to)
{
return new Money(0, to);
}
public IExpression Times(int multiplier)
{
return this;
}
}
}
Since only one neutral element can exist, it makes sense to make it a singleton . The private class
PlusIdentity
is a new implementation IExpression
that does nothing. The method
Plus
simply returns the input value. This is the same behavior as adding numbers. When added, zero is a neutral element, and the same thing happens here. This is more clearly seen in the method Reduce
, where the calculation of the “neutral” currency is simply reduced to zero in the requested currency. Finally, if you multiply the neutral element by something, you get the neutral element. Here, interestingly, it PlusIdentity
behaves similarly to the neutral element for the multiplication operation (1). Now we will write tests for anyone
IExpression
x
:Assert.Equal(x, x.Plus(Plus.Identity), Compare.UsingBank);
Assert.Equal(x, Plus.Identity.Plus(x), Compare.UsingBank);
This is a property test, and it runs for all
x
generated by FsCheck. The caution applicable to associativity is also applicable here: it IExpression
is an interface, so you cannot be sure that it Plus.Identity
will be a neutral element for all implementations IExpression
that someone can create, but for the three existing implementations the monoid laws are preserved. Now we can say that the operation
IExpression.Plus
is a monoid.Multiplication
In arithmetic, the multiplication operator is called "times" (in English "times" - approx. Per. ). When you write 3 * 5 , it literally means that you have
3
five times (or 5
three times?). In other words: 3 * 5 = 3 + 3 + 3 + 3 + 3
Is there a similar operation for
IExpression
? Perhaps we can find a hint in the Haskell language, where monoids and semigroups are part of the main library. Later you will learn about semigroups, but for now, just note that the class
Semigroup
defines a function stimes
that has a type Integral b => b -> a -> a
. This means that for any integer type (16-bit integer, 32-bit integer, etc.) the function stimes
takes an integer and a value a
and "multiplies" the value by a number. Herea
- a type for which a binary operation exists. In C #, a function
stimes
will look like a class method Foo
:public Foo Times(int multiplier)
I called the method
Times
, not STimes
because I strongly suspect that the letter s
in the name stimes
means Semigroup
. And note that this method has the same signature as the method IExpression.Times
. If you can define a universal implementation of such a function in Haskell, is it possible to do the same in C #? In the class,
Money
we can implement Times
using the method Plus
:public IExpression Times(int multiplier)
{
return Enumerable
.Repeat((IExpression)this, multiplier)
.Aggregate((x, y) => x.Plus(y));
}
The
Repeat
LINQ static method returns this
as many times as specified in multiplier
. The return value is , but in accordance with the interface should return a single value . Use a method for combining multiple two values ( and ) one using the method .
This implementation is unlikely to be as effective as the previous, specific implementation, but here we are not talking about efficiency, but about a general, reused abstraction. Exactly the same implementation can be used for the method :Enumerable
IExpression
Times
IExpression
Aggregate
IExpression
x
y
Plus
Sum.Times
public IExpression Times(int multiplier)
{
return Enumerable
.Repeat((IExpression)this, multiplier)
.Aggregate((x, y) => x.Plus(y));
}
This is exactly the same code as for
Money.Times
. You can also copy and paste this code into PlusIdentity.Times
, but I will not repeat it here, because it is the same code as above. This means that you can remove the method
Times
from IExpression
:public interface IExpression
{
Money Reduce(Bank bank, string to);
IExpression Plus(IExpression addend);
}
instead, implementing it as an extension method :
public static class Expression
{
public static IExpression Times(this IExpression exp, int multiplier)
{
return Enumerable
.Repeat(exp, multiplier)
.Aggregate((x, y) => x.Plus(y));
}
}
This will work because any object
IExpression
has a method Plus
. As I said, this is likely to be less effective than specialized implementations
Times
. In Haskell, this is eliminated by inclusion stimes
in the typeclass , so developers can implement a more efficient algorithm than the default implementation. In C #, the same effect can be achieved by reorganizing IExpression
into an abstract base class using Times
as a public virtual (overridable) method.Validation check
Since language Haskell has a more formal definition of monoid, we can try to rewrite the API Kent Beck in Haskell, just as a proof of the idea itself ( proof of concept ). In my last modification, my fork in C # has three implementations
IExpression
:Money
Sum
PlusIdentity
Since interfaces are extensible, we need to take care of this, so in Haskell it seems to me safer to implement these three subtypes as a type
sum
:data Expression = Money { amount :: Int, currency :: String }
| Sum { augend :: Expression, addend :: Expression }
| MoneyIdentity
deriving (Show)
More formally, we can do this using
Monoid
instance Monoid Expression where
mempty = MoneyIdentity
mappend MoneyIdentity y = y
mappend x MoneyIdentity = x
mappend x y = Sum x y
The method
Plus
from our C # example is represented here by a function mappend
. The only remaining member of the class IExpression
is a method Reduce
that can be implemented as follows:import Data.Map.Strict (Map, (!))
reduce :: Ord a => Map (String, a) Int -> a -> Expression -> Int
reduce bank to (Money amt cur) = amt `div` rate
where rate = bank ! (cur, to)
reduce bank to (Sum x y) = reduce bank to x + reduce bank to y
reduce _ _ MoneyIdentity = 0
Everything else will be taken care of by the typclass mechanism, so now we can reproduce one of Kent Beck's tests as follows:
λ> let bank = fromList [(("CHF","USD"),2), (("USD", "USD"),1)]
λ> let sum = stimesMonoid 2 $ MoneyPort.Sum (Money 5 "USD") (Money 10 "CHF")
λ> reduce bank "USD" sum
20
Just as it
stimes
works for anyone Semigroup
, it’s stimesMonoid
specific for anyone Monoid
, and therefore we can also use it with Expression
. With a historical exchange rate of 2: 1, “$ 5 + 10 Swiss francs multiplied by 2” will be exactly $ 20.
Summary
In the 17th chapter of his book, Kent Beck describes how he repeatedly tried to come up with various variants of the Money API before trying to make it “on expressions,” which he eventually used in the book. In other words, he had a lot of experience, both with this particular problem and with programming in general. Obviously, this work was done by a highly qualified programmer.
And it seemed to me curious that he seems to intuitively come to "monoid design." Perhaps he did it on purpose (he does not talk about this in the book), so I would rather assume that he came to this design simply because he realized its superiority. It is for this reason that it seems interesting to me to consider specifically thisan example, like a monoid, because it gives the idea that there is something highly understandable with respect to the monoid-based API. Conceptually, this is simply a “small addition."
In this article, we returned to the code of nine years ago (in fact, 15 years old - approx. Lane ) to identify it as a monoid. In the next article, I am going to revise the code for 2015.
Conclusion
This concludes this article. There is still a lot of information that will be published in the same way as in the original - in the form of consecutive posts on the Habré linked by backlinks. Hereinafter: the original articles are Mark Seemann 2017, translations are done by the java community, the translator is Evgeny Fedorov.