I am working on a project where we work with some angles. Some of the angles are stored as radians and some are stored as degrees. Storing everything as either radians or degrees is an undesirable option.
To fix the unit I am using a concept called newtype
in functional programming languages. This stackoverflow question explained how to implement something similar in C#.
Here is the working code I have which is working perfectly.
// Some constants, may get them from another place in production code.
internal abstract class Constants {
public const float RadiansPerCircle = 2.0f*(float) System.Math.PI;
public const float DegreesPerCircle = 360.0f;
public const float RadiansPerDegree = RadiansPerCircle/DegreesPerCircle;
public const float DegreesPerRadian = DegreesPerCircle/RadiansPerCircle;
}
public interface IAngle {
float AsRadians { get; }
float AsDegrees { get; }
}
// A struct is used instead of a class because we want a very thin wrapper
// around a float value. In functional programming languages this is usually
// called a `newtype`.
public struct Radians : IAngle {
// The actual value is stored in the radians property.
public float AsRadians { get; }
// The degrees are computer from the radians property every time it is
// requested. If the degrees are requested more commonly than the
// radians, the `Radians` value should be used to store the value
// instead.
public float AsDegrees { get { return AsRadians*Constants.DegreesPerRadian; } }
// The constructor trusts that the passed in float is representing an
// angle in radians.
public Radians(float radians) { this.AsRadians = radians; }
// A static factory method is used instead of a constructor because
// constructors do not support generic parameters. If we would not use
// generics and simply take an IAngle as a parameter, the value has to
// be boxed which is not what we want.
public static Radians from<TAngle>(TAngle angle) where TAngle : IAngle {
return new Radians(angle.AsRadians);
}
}
public struct Degrees : IAngle {
public float AsRadians { get { return AsDegrees*Constants.RadiansPerDegree; } }
public float AsDegrees { get; }
public Degrees(float degrees) { this.AsDegrees = degrees; }
public static Degrees from<TAngle>(TAngle angle) where TAngle : IAngle {
return new Degrees(angle.AsDegrees);
}
}
In some places we need the degrees or radians within the range of a full circle. In other words, we want to limit the degrees to between 0 and 360 or the radians to between 0 and 2 PI.
In order not to repeat the calculations we want to create a variant of the Radians
type called WrappedRadians
of which we know that the value is already limited to the domain of a full circle.
The code exploded:
// Some constants, may get them from another place in production code.
internal abstract class Constants {
public const float RadiansPerCircle = 2.0f*(float) System.Math.PI;
public const float DegreesPerCircle = 360.0f;
public const float RadiansPerDegree = RadiansPerCircle/DegreesPerCircle;
public const float DegreesPerRadian = DegreesPerCircle/RadiansPerCircle;
}
public interface IAngle {
float AsRadians { get; }
float AsWrappedRadians { get; }
float AsDegrees { get; }
float AsWrappedDegrees { get; }
}
public struct Radians : IAngle {
public float AsRadians { get; }
public float AsWrappedRadians { get { return AsRadians % Constants.RadiansPerCircle; } }
public float AsDegrees { get { return AsRadians*Constants.DegreesPerRadian; } }
public float AsWrappedDegrees { get { return AsWrappedRadians*Constants.DegreesPerRadian; } }
public Radians(float AsRadians) { this.AsRadians = AsRadians; }
public static Radians from<TAngle>(TAngle angle) where TAngle : IAngle {
return new Radians(angle.AsRadians);
}
}
public struct WrappedRadians : IAngle {
public float AsRadians { get { return AsWrappedRadians; } }
public float AsWrappedRadians { get; }
public float AsDegrees { get { return AsWrappedDegrees; } }
public float AsWrappedDegrees { get { return AsWrappedRadians*Constants.DegreesPerRadian; } }
public WrappedRadians(float AsRadians) { this.AsWrappedRadians = AsRadians % Constants.RadiansPerCircle; }
public static WrappedRadians from<TAngle>(TAngle angle) where TAngle : IAngle {
return new WrappedRadians(angle.AsWrappedRadians);
}
}
public struct Degrees : IAngle {
public float AsRadians { get { return AsDegrees*Constants.RadiansPerDegree; } }
public float AsWrappedRadians { get { return AsWrappedDegrees*Constants.RadiansPerDegree; } }
public float AsDegrees { get; }
public float AsWrappedDegrees { get { return AsDegrees % Constants.DegreesPerCircle; } }
public Degrees(float AsDegrees) { this.AsDegrees = AsDegrees; }
public static Degrees from<TAngle>(TAngle angle) where TAngle : IAngle {
return new Degrees(angle.AsDegrees);
}
}
public struct WrappedDegrees : IAngle {
public float AsRadians { get { return AsWrappedRadians; } }
public float AsWrappedRadians { get { return AsWrappedDegrees*Constants.DegreesPerRadian; } }
public float AsDegrees { get { return AsWrappedDegrees; } }
public float AsWrappedDegrees { get; }
public WrappedDegrees(float AsDegrees) { this.AsWrappedDegrees = AsDegrees % Constants.DegreesPerCircle; }
public static WrappedDegrees from<TAngle>(TAngle angle) where TAngle : IAngle {
return new WrappedDegrees(angle.AsWrappedDegrees);
}
}
While the implementation is pretty neat on one hand, there is a fair amount of duplication. That could be worked around if default method implementations, value type (struct) inheritance, or macros were a thing. Due to the duplication, the implementation leaves lots of room for error. It is easy to use the wrong constant or property by accident. Of course this can be mitigated by doing rigorous unit testing but that is not tackling the source of the problem.
Then, we discovered that we also wanted a type called Circles
that stored the angle as a fraction of a circle. Of course we'd also want WrappedCircles
. You can imagine the increase in code size.
Problem statement
We have values that are used in different units. To work with them we are looking for an implementation that satisfies the following requirements.
- The abstraction must be zero-cost.
- It must be possible to store the values in any unit.
- It must be possible to get a value in any unit, regardless of in which unit the value is stored.
- It must not be possible to do erroneous calculations because values are in different units.
- The amount of code duplication must be limited.
I realize whether or not the requirements are good ones is debatable. This is however not the subject of this question.
If we drop one of the requirements, the solution becomes trivial. For example, we can drop requirement 2 by simply implementing only the Radians
struct from the examples. Please refrain from questioning the requirements and focus on how to implement something that satisfies all of them.
The code I present in the examples satisfy all but requirement 5. What would help in keeping the code size down would be:
- inheritance for structs
- default implementations of methods in interfaces
- macros
Unfortunately, it seems C# does not offer any of these. My question comes down to: am I missing some C# feature that could be used or is there a different implementation design?
I expect that there is no other C# feature that I missed and can be utilized. I expect that there might be another design which satisfies all the requirements, but I have not thought of it yet.
In response to the suggestion to utilize conversion operators
Conversion operators make the types nicer to work with but they do not solve the duplication problem. The following implementation shows what the conversion operators can do for you and how they relate to the previously given code.
internal abstract class Constants {
public const float RadiansPerRevolution = (float) (2*System.Math.PI);
public const float RevolutionsPerRadian = 1/RadiansPerRevolution;
public const float DegreesPerRevolution = 360;
public const float RevolutionsPerDegree = 1/DegreesPerRevolution;
public const float RadiansPerDegree = RadiansPerRevolution/DegreesPerRevolution;
public const float DegreesPerRadian = DegreesPerRevolution/RadiansPerRevolution;
}
public interface IAngularUnit {
float InDegrees { get; }
float InRadians { get; }
float InRevolutions { get; }
}
public struct Degrees : IAngularUnit {
public float InDegrees { get; }
public float InRadians { get { return Constants.RadiansPerDegree*InDegrees; } }
public float InRevolutions { get { return Constants.RevolutionsPerDegree*InDegrees; } }
private Degrees(float degrees) { InDegrees = degrees; }
public static implicit operator Degrees(float value) { return new Degrees(value); }
public static explicit operator float(Degrees degrees) { return degrees.InDegrees; }
public static Degrees From<TAngularUnit>(TAngularUnit angle) where TAngularUnit : IAngularUnit { return angle.InDegrees; }
public static implicit operator Degrees(Radians radians) { return Degrees.From(radians); }
public static implicit operator Degrees(Revolutions revolutions) { return Degrees.From(revolutions); }
}
public struct Radians : IAngularUnit {
public float InDegrees { get { return Constants.DegreesPerRadian*InRadians; } }
public float InRadians { get; }
public float InRevolutions { get { return Constants.RevolutionsPerRadian*InRadians; } }
private Radians(float radians) { InRadians = radians; }
public static implicit operator Radians(float value) { return new Radians(value); }
public static explicit operator float(Radians radians) { return radians.InRadians; }
public static Radians From<TAngularUnit>(TAngularUnit angle) where TAngularUnit : IAngularUnit { return angle.InRadians; }
public static implicit operator Radians(Degrees degrees) { return Radians.From(degrees); }
public static implicit operator Radians(Revolutions revolutions) { return Radians.From(revolutions); }
}
public struct Revolutions : IAngularUnit {
public float InRevolutions { get; }
public float InDegrees { get { return Constants.DegreesPerRevolution*InRevolutions; } }
public float InRadians { get { return Constants.RadiansPerRevolution*InRevolutions; } }
private Revolutions(float revolutions) { InRevolutions = revolutions; }
public static implicit operator Revolutions(float revolutions) { return new Revolutions(revolutions); }
public static explicit operator float(Revolutions revolutions) { return revolutions.InRevolutions; }
public static Revolutions From<TAngularUnit>(TAngularUnit angle) where TAngularUnit : IAngularUnit { return angle.InRevolutions; }
public static implicit operator Revolutions(Degrees degrees) { return Revolutions.From(degrees); }
public static implicit operator Revolutions(Radians radians) { return Revolutions.From(radians); }
}
public class Entry {
public static void Main() {
Radians r = 3.14f;
Degrees d = 359;
// These conversions lose information on the unit, therefore
// explicit casts are required.
float rf = (float) r;
float df = (float) d;
// These should convert between units without losing information.
Degrees r_as_d = r;
Radians d_as_r = d;
// These all work, but the overload variant requires a duplicated
// implementation and the boxed variant boxes the angle.
PrintInRadiansOverload(r);
PrintInRadiansOverload(d);
PrintInRadians(r);
PrintInRadians(d);
PrintInRadiansBoxed(r);
PrintInRadiansBoxed(d);
}
public static void PrintInRadiansOverload(Radians radians) {
System.Console.WriteLine("{0:F2} rad", (float) radians);
}
public static void PrintInRadiansOverload(Degrees degrees) {
// Error prone because of duplication.
System.Console.WriteLine("{0:F2} rad", (float) (Radians) degrees);
}
public static void PrintInRadians<TAngularUnit>(TAngularUnit angle) where TAngularUnit : IAngularUnit {
System.Console.WriteLine("{0:F2} rad", angle.InRadians);
}
public static void PrintInRadiansBoxed(IAngularUnit angle) {
System.Console.WriteLine("{0:F2} rad", angle.InRadians);
}
}
Note that I chose to define the conversions in the InRadians
. It is also possible to define the conversions in the conversion operators like so.
public struct Degrees : IAngularUnit {
public float InDegrees { get; }
public float InRadians { get { return (float) (Radians) this; } }
public float InRevolutions { get { return (float) (Revolutions) this; } }
private Degrees(float degrees) { InDegrees = degrees; }
public static implicit operator Degrees(float value) { return new Degrees(value); }
public static explicit operator float(Degrees degrees) { return degrees.InDegrees; }
public static Degrees From<TAngularUnit>(TAngularUnit angle) where TAngularUnit : IAngularUnit { return angle.InDegrees; }
public static implicit operator Degrees(Radians radians) { return Constants.DegreesPerRadian*(float) radians; }
public static implicit operator Degrees(Revolutions revolutions) { return Constants.DegreesPerRevolution*(float) revolutions; }
}
As long as the chosen implementation is consistent between the units, the implementations are equivalent.