
public interface IExpression { Money Reduce(Bank bank, string to); IExpression Plus(IExpression addend); IExpression Times(int multiplier); } Reduce method converts an IExpression object to a certain currency (to parameter), represented as a Money object. This is useful if you have an expression that has multiple currencies.Plus method adds an IExpression object to the current IExpression object and returns a new IExpression . It can be money in one currency or in several.Times method multiplies IExpression by a specific multiplier. You probably noticed that in all the examples we use integers for the multiplier and the sum. I think Kent Beck did this in order not to complicate the code, but in real life we ​​would use fractional numbers when working with money (for example, decimal ).IExpression interface IExpression two classes:Money represents a certain amount of money in a particular currency. It contains the properties “Amount” (quantity) and “Currency” (name of currency). This is a key point: Money is a value object .Sum is the sum of two other IExpression objects. It contains two terms, called Augend (first term) and Addend (second term). IExpression sum = new Sum(Money.Dollar(5), Money.Franc(10)); Money.Dollar and Money.Franc are two static factory methods that return Money objects.Plus is a binary operation? Can we consider it a monoid?IExpression objects, x , y and z , the expression x.Plus(y).Plus(z) must be equal to x.Plus(y.Plus(z)) . How should we understand equality here? The return value of the Plus method is the IExpression interface, and the interfaces have no such thing as equality. So, either, equality depends on specific implementations ( Money and Sum ), where we can define appropriate methods, or we can use test matching (test pattern, test-specific equality , - approx. Lane ).IExpression !Reduce method can convert any IExpression to an object of type Money (that is, to a single currency), and since Money is an object-value, it has structural equality (for more information about value objects and their features, you can read, for example, here ). And we can use this property to compare IExpression objects. All we need is the exchange rate.Reduce and xUnit.net as the IEqualityComparer<IExpression> class: public class ExpressionEqualityComparer : IEqualityComparer<IExpression> { 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(); } } Bank object with a 2: 1 exchange rate. The Bank class is another object from the Kent Beck code. It itself does not implement any interface, but is used as an argument to the Reduce method. public static class Compare { public static ExpressionEqualityComparer UsingBank = new ExpressionEqualityComparer(); } Assert.Equal( x.Plus(y).Plus(z), x.Plus(y.Plus(z)), Compare.UsingBank); Sum and Money objects that FsCheck generates.IExpression.Plus associative, but it's worth noting that this behavior is not guaranteed, and here's why: IExpression is an interface, so someone can easily add a third implementation that will break the associativity. Conventionally, we will assume that the operation Plus associative, but the situation is delicate.IExpression.Plus associative, then this is a candidate for monoids. If there exists a neutral element, then this is definitely a monoid. 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; } } } PlusIdentity is a new implementation of IExpression that does nothing.Plus method simply returns the input value. This is the same behavior as for adding numbers. When added, zero is a neutral element, and the same thing happens here. This is more clearly seen in the Reduce method, where the calculation of a “neutral” currency is simply reduced to zero in the requested currency. Finally, if you multiply a neutral element by something, you get a neutral element. Here, interestingly, PlusIdentity behaves like a neutral element for a multiplication operation (1).IExpression x : Assert.Equal(x, x.Plus(Plus.Identity), Compare.UsingBank); Assert.Equal(x, Plus.Identity.Plus(x), Compare.UsingBank); x generated by FsCheck. Caution applied to associativity is also applicable here: IExpression is an interface, so you cannot be sure that Plus.Identity will be a neutral element for all implementations of IExpression that anyone can create, but for three existing implementations, monoid laws are preserved.IExpression.Plus is a monoid.3 five times (or 5 three times?). In other words:3 * 5 = 3 + 3 + 3 + 3 + 3IExpression ?Semigroup class defines the stimes function, which is of type Integral b => b -> a -> a . This means that for any integer type (16-bit integer, 32-bit integer, etc.), the stimes function takes an integer and a value a and “multiplies” the value by a number. Here a is the type for which there is a binary operation.stimes function will look like a method of the Foo class: public Foo Times(int multiplier) 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 IExpression.Times method.Money class, we can implement Times using the Plus method: public IExpression Times(int multiplier) { return Enumerable .Repeat((IExpression)this, multiplier) .Aggregate((x, y) => x.Plus(y)); } Repeat method returns this as many times as specified in the multiplier . The return value is an Enumerable<IExpression> , but according to the IExpression interface, IExpression should return one IExpression value. We use the Aggregate method to repeatedly merge two IExpression values ​​( x and y ) into one using the Plus method.Sum.Times method: public IExpression Times(int multiplier) { return Enumerable .Repeat((IExpression)this, multiplier) .Aggregate((x, y) => x.Plus(y)); } 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.Times method from IExpression : public interface IExpression { Money Reduce(Bank bank, string to); IExpression Plus(IExpression addend); } public static class Expression { public static IExpression Times(this IExpression exp, int multiplier) { return Enumerable .Repeat(exp, multiplier) .Aggregate((x, y) => x.Plus(y)); } } IExpression object has a Plus method.Times . In Haskell, this is eliminated by including stimes in the type class ( 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 the Times as a public virtual (overridable) method.IExpression :MoneySumPlusIdentitysum : data Expression = Money { amount :: Int, currency :: String } | Sum { augend :: Expression, addend :: Expression } | MoneyIdentity deriving (Show) Monoid instance Monoid Expression where mempty = MoneyIdentity mappend MoneyIdentity y = y mappend x MoneyIdentity = x mappend xy = Sum xy Plus method from our C # example here is represented by the mappend function. The only remaining member of the IExpression class is the Reduce method, which 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 xy) = reduce bank to x + reduce bank to y reduce _ _ MoneyIdentity = 0 λ> 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 stimes works for any Semigroup , stimesMonoid defined for any Monoid , and therefore we can also use it with Expression .Source: https://habr.com/ru/post/341398/
All Articles