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:


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 Reduceconverts the object IExpressionto a certain currency (parameter to), represented as an object Money. This is useful if you have an expression that has multiple currencies.

The method Plusadds the object IExpressionto the current object IExpressionand returns a new one IExpression. It can be money in one currency or in several.

The method Timesmultiplies IExpressionby 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 IExpressiontwo classes:

  • Moneyrepresents a certain amount of money in a specific currency. It contains the “Amount” (quantity) and “Currency” (currency name) properties. This - the key point: Moneyis the value objects ( of value object ).
  • Sumis the sum of two other objects IExpression. 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.Dollarand Money.Francare two static factory methods that return objects Money.

Associativity


Have you noticed that Plusthis 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, yand 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 Plusis an interface IExpression, and interfaces do not have such a thing as equality. Therefore, either equality depends on specific implementations ( Moneyand 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 Reducecan convert any IExpressioninto an object of type Money(that is, to a single currency), and since it Moneyis 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 Reduceand 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 Bankwith a 2: 1 exchange rate. A class Bankis 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 Sumand Moneythat 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 IExpressionis an interface, so someone can easily add a third implementation that will violate associativity. Conventionally, we will assume that the operation is Plusassociative, but the situation is delicate.

Neutral element


If we accept that it is IExpression.Plusassociative, 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 PlusIdentityis a new implementation IExpressionthat does nothing.

The method Plussimply 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 PlusIdentitybehaves similarly to the neutral element for the multiplication operation (1).

Now we will write tests for anyone IExpressionx:

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 xgenerated by FsCheck. The caution applicable to associativity is also applicable here: it IExpressionis an interface, so you cannot be sure that it Plus.Identitywill be a neutral element for all implementations IExpressionthat someone can create, but for the three existing implementations the monoid laws are preserved.

Now we can say that the operation IExpression.Plusis 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 3five times (or 5three 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 Semigroupdefines a function stimesthat has a type Integral b => b -> a -> a. This means that for any integer type (16-bit integer, 32-bit integer, etc.) the function stimestakes an integer and a value aand "multiplies" the value by a number. Herea- a type for which a binary operation exists.

In C #, a function stimeswill look like a class method Foo:

public Foo Times(int multiplier)

I called the method Times, not STimesbecause I strongly suspect that the letter sin the name stimesmeans 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, Moneywe can implement Timesusing the method Plus:

public IExpression Times(int multiplier)
{
    return Enumerable
        .Repeat((IExpression)this, multiplier)
        .Aggregate((x, y) => x.Plus(y));
}

The RepeatLINQ static method returns thisas 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 :EnumerableIExpressionTimesIExpressionAggregateIExpressionxyPlus

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 Timesfrom 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 IExpressionhas a method Plus.

As I said, this is likely to be less effective than specialized implementations Times. In Haskell, this is eliminated by inclusion stimesin the typeclass , so developers can implement a more efficient algorithm than the default implementation. In C #, the same effect can be achieved by reorganizing IExpressioninto an abstract base class using Timesas 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 Plusfrom our C # example is represented here by a function mappend. The only remaining member of the class IExpressionis a method Reducethat 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 stimesworks for anyone Semigroup, it’s stimesMonoidspecific 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.

Also popular now: