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 + 3
IExpression
?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
:Money
Sum
PlusIdentity
sum
: 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