Representation Bugs with Typed Languages
In many systems, critical semantic distinctions exist only in the minds of programmers. For example, take the following C code from gpsutils.c of the gpsd project[1]
// Distance in meters between two points specified in degrees.
double earth_distance(double lat1, double lon1, double lat2, double lon2);
This is an “imagined abstraction”: the fact that lat and lon are expressed in degrees
rather than radians or another unit is documented only in prose, not in the type
system.
A small bug arising from the lack of such formal abstractions might look like this:
// Position A: already in decimal degrees
double latA_deg = 51.2345;
double lonA_deg = −0.1234;
// Position B: latitude/longitude in NMEA ddmm.mmmm format
// ”4807.038” means 48 degrees + 07.038 arcminutes
double latB_nmea = 4807.038; // raw NMEA
double lonB_nmea = 1131.000; // raw NMEA
// BUG: forgot to convert NMEA values into decimal degrees
double d = earth_distance(
latA_deg, lonA_deg,
latB_nmea, lonB_nmea, // <−− bug
);
This code compiles and runs, but the results are nonsensical because latB_nmea and lonB_nmea are still in degrees+minutes format, not decimal degrees.
The NMEA standard 2 expresses coordinates as ddmm.mmmm, i.e. the value is equivalent to
v = whole_degrees ∗ 100 + arcminutes, so they must first be parsed and converted back
to a single degrees value by:
degrees = v 100 + (v - floor(v)/100c)
Mixing incompatible representations (like degrees vs. radians, arcminutes, NMEA angles) or even just swapping latitude and longitude is only detectable through careful documentation reading and programmer discipline.
A nearly identical bug was reported in the Meshtastic project, where an NMEA sentence was mistakenly output in decimal degrees instead of the standard ddmm.mmmm format. Before the bug was identified and fixed, the results had position errors on the order of tens of kilometers.
Traditional type systems offer two common solutions to this problem, each with its own drawbacks.
Wrapper Types
One is to introduce wrapper types:
struct Meters { double value; };
struct Degrees { double value; };
struct Latitude { Degrees deg; };
struct Longitude { Degrees deg; };
Meters earth_distance(
Latitude lat1, Longitude lon1,
Latitude lat2, Longitude lon2
);
While this approach helps encode meaning in types, it also introduces friction. Even conceptually equivalent types like Meters or Feet would remain incompatible at the type level unless explicitly converted. As more aspects of representation vary (such as units, coordinate systems, time formats or underlying representations float vs. double vs. ascii), additional types accumulate, making code harder to read, write, and maintain. The result is often a proliferation of boilerplate and subtle type mismatches. Templated Wrappers More genericity can be achieved by adding templates to make wrapper types flexible with regard to their underlying representation at the cost of cluttered code with a lot of boilerplate overhead.
template< typename T > struct Meters{ T value; };
template< typename T > struct Degrees{ T value; };
struct NMEA_AngleAscii { std::string value; };
struct NMEA_AngleFloat { float value; };
template< typename Angle > struct Latitude{ Angle value; };
template< typename Angle > struct Longitude{ Angle value; };
Meters<double> earth_distance(
Latitude<Degrees<float>> lat1, Longitude<Degrees<float>> lon1,
Latitude<Degrees<float>> lat2, Longitude<Degrees<float>> lon2
);
Dynamic Dispatch
Dynamic dispatch avoids the need for templated duplication, but brings its own costs. It introduces runtime overhead, discards the ability to enforce type correctness at compile time, and typically requires heap allocation or smart pointers. Moreover, it breaks intuitive value semantics, transforming what should be a simple data value into a more complex, reference-based structure.
struct Length { virtual double to_meters() = 0; };
struct Angle { virtual double to_degrees() = 0; };
struct Latitude : public Angle { };
struct Longitude : public Angle { };
Length* earth_distance(
Latitude* lat1, Longitude* lon1,
Latitude* lat2, Longitude* lon2
);
Despite the appeal of stronger type safety, wrapper types and dynamic dispatch introduce complexity, indirection, and potential performance costs that are undesirable in low-level, resource-sensitive environments. Projects like GPSD, which prioritize portability, zero-overhead abstractions, and tight control over memory layouts, often avoid these approaches entirely. Instead, they rely on plain double values for latitude and longitude, with the intended meaning expressed in comments. This keeps the code simple, fast, and compatible with C APIs — but at the cost of shifting the burden of correctness onto careful reading and programmer discipline.
Footnotes
[1]: gpsd is a software abstraction layer on linux for interfacing with gps receivers from various venodrs through a unified interface
[2]: The NMEA 0183 standard, which is commonly used to communicate with GPS receiver devices, is an ASCII-based serial communications protocol that besides other things, provides an interface to query GPS-related information. For further details see:
[3]: Meshtastic firmware issue #1931