Angular addition monoid by Mark Seemann
Geometric angles can be added together. Angular addition forms a monoid.
This article is part of a series about monoids. In short, a monoid is an associative binary operation with a neutral element (also known as identity).
In geometry, an angle is a measure of how two crossing lines relate to each other. In mathematics, angles are usually represented in radians, but in daily use, they're mostly measured in degrees between 0 and 360.
Angular addition #
You can always draw an angle within a circle. Here's a 45° angle:
If you add another 90° angle to that, you get a 135° angle:
What do you get if you add 90° to 315°?
Well, you get 45°, of course!
There's only 360° in a circle, so overflow is handled, in this case, by subtracting 360°. In general, however, angular addition is nothing but modulo 360 addition.
Angle struct #
You can model a geometric angle as a struct. Here's a simple example:
public struct Angle { private readonly decimal degrees; private Angle(decimal degrees) { this.degrees = degrees % 360m; if (this.degrees < 0) this.degrees += 360m; } public static Angle FromDegrees(decimal degrees) { return new Angle(degrees); } public static Angle FromRadians(double radians) { return new Angle((decimal)((180D / Math.PI) * radians)); } public Angle Add(Angle other) { return new Angle(this.degrees + other.degrees); } public readonly static Angle Identity = new Angle(0); public override bool Equals(object obj) { if (obj is Angle) return ((Angle)obj).degrees == this.degrees; return base.Equals(obj); } public override int GetHashCode() { return this.degrees.GetHashCode(); } public static bool operator ==(Angle x, Angle y) { return x.Equals(y); } public static bool operator !=(Angle x, Angle y) { return !x.Equals(y); } }
Notice the Add
method, which is a binary operation; it's an instance method on Angle
, takes another Angle
as input, and returns an Angle
value.
Associativity #
Not only is Add
a binary operation; it's also associative. Here's an example:
var x = Angle.FromDegrees(135); var y = Angle.FromDegrees(180); var z = Angle.FromDegrees(300); var left = x.Add(y).Add(z); var right = x.Add(y.Add(z));
Notice that left
first evaluates x.Add(y)
, which is 315°; then it adds 300°, which is 615°, but normalises to 255°. On the other hand, right
first evaluates y.Add(z)
, which is 480°, but normalises to 120°. It then adds those 120° to x
, for a final result of 255°. Since left
and right
are both 255°, this illustrates that Add
is associative.
Obviously, this is only a single example, so it's no proof. While still not a proof, you can demonstrate the associativity property with more confidence by writing a property-based test. Here's one using FsCheck and xUnit.net:
[Property(QuietOnSuccess = true)] public void AddIsAssociative(Angle x, Angle y, Angle z) { Assert.Equal( x.Add(y).Add(z), x.Add(y.Add(z))); }
By default, FsCheck generates 100 test cases, but even when I experimentally change the configuration to run 100,000 test cases, they all pass. For full disclosure, however, I'll admit that I defined the data generators to only use NormalFloat
for the radian values, and only decimal
values with up to 10 decimal places. If you try to use entirely unconstrained floating points, you'll see test failures caused by rounding errors.
Changing the data generator is one way to address rounding errors. Another way is to add a bit of fuzzy tolerance to the assertion. In any case, though, the Add
operation is associative. That rounding errors occur is an implementation detail of floating point arithmetic.
Identity #
The above code listing defines a value called Identity
:
public readonly static Angle Identity = new Angle(0);
As an Angle, I want my Add and Identity members to obey the monoid laws so that I can be a monoid.
As an example, both left
and right
should be true
in the following:
var x = Angle.FromDegrees(370); var left = x == Angle.Identity.Add(x); var right = x == x.Add(Angle.Identity);
That does, indeed, turn out to be the case.
Again, you can generalise using FsCheck:
[Property(QuietOnSuccess = true)] public void AddHasIdentity(Angle x) { Assert.Equal(x, Angle.Identity.Add(x)); Assert.Equal(x, x.Add(Angle.Identity)); }
Once more, a reservation identical to the one given above must be given when it comes to floating point arithmetic.
Conclusion #
The Add
method is an associative, binary operation with identity; it's a monoid.
As far as I can tell, any modulo-based addition is a monoid, but while, say, modulo 37 addition probably doesn't have any practical application, modulo 360 addition does, because it's how you do angular addition.