gluecodium icon indicating copy to clipboard operation
gluecodium copied to clipboard

When generating equality operator for Float and Double properties, consider an epsilon

Open OguzHere opened this issue 2 years ago • 3 comments

In case of Float and Double properties, the equality check should utilise an epsilon as exact equality check is misleading. For example, we have a SpeedLimit struct:

@Equatable
struct SpeedLimit {
    special_speed_situations: List<SpecialSpeedSituation> = []
    advisory_speed_limit_in_meters_per_second: Double? = null
    snow_speed_limit_in_meters_per_second: Double? = null
    rain_speed_limit_in_meters_per_second: Double? = null
    fog_speed_limit_in_meters_per_second: Double? = null
    optimal_weather_speed_limit_in_meters_per_second: Double? = null
    school_zone_speed_limit_in_meters_per_second: Double? = null
    time_dependent_speed_limit_in_meters_per_second: Double? = null
    fun effective_speed_limit_in_meters_per_second(): Double?
...
}

The Gluecodium generated equality operator:

bool SpeedLimit::operator==( const SpeedLimit& rhs ) const
{
    return ( ( speedLimitInMetersPerSecond && rhs.speedLimitInMetersPerSecond )
                ? ( *speedLimitInMetersPerSecond == *rhs.speedLimitInMetersPerSecond )
                : ( static_cast< bool >( speedLimitInMetersPerSecond ) == static_cast< bool >( rhs.speedLimitInMetersPerSecond ) ) ) &&
            specialSpeedSituations == rhs.specialSpeedSituations &&
            ( ( advisorySpeedLimitInMetersPerSecond && rhs.advisorySpeedLimitInMetersPerSecond )
                ? ( *advisorySpeedLimitInMetersPerSecond == *rhs.advisorySpeedLimitInMetersPerSecond )
                : ( static_cast< bool >( advisorySpeedLimitInMetersPerSecond ) == static_cast< bool >( rhs.advisorySpeedLimitInMetersPerSecond ) ) ) &&
            ( ( snowSpeedLimitInMetersPerSecond && rhs.snowSpeedLimitInMetersPerSecond )
                ? ( *snowSpeedLimitInMetersPerSecond == *rhs.snowSpeedLimitInMetersPerSecond )
                : ( static_cast< bool >( snowSpeedLimitInMetersPerSecond ) == static_cast< bool >( rhs.snowSpeedLimitInMetersPerSecond ) ) ) &&
            ( ( rainSpeedLimitInMetersPerSecond && rhs.rainSpeedLimitInMetersPerSecond )
                ? ( *rainSpeedLimitInMetersPerSecond == *rhs.rainSpeedLimitInMetersPerSecond )
                : ( static_cast< bool >( rainSpeedLimitInMetersPerSecond ) == static_cast< bool >( rhs.rainSpeedLimitInMetersPerSecond ) ) ) &&
            ( ( fogSpeedLimitInMetersPerSecond && rhs.fogSpeedLimitInMetersPerSecond )
                ? ( *fogSpeedLimitInMetersPerSecond == *rhs.fogSpeedLimitInMetersPerSecond )
                : ( static_cast< bool >( fogSpeedLimitInMetersPerSecond ) == static_cast< bool >( rhs.fogSpeedLimitInMetersPerSecond ) ) ) &&
            ( ( optimalWeatherSpeedLimitInMetersPerSecond && rhs.optimalWeatherSpeedLimitInMetersPerSecond )
                ? ( *optimalWeatherSpeedLimitInMetersPerSecond == *rhs.optimalWeatherSpeedLimitInMetersPerSecond )
                : ( static_cast< bool >( optimalWeatherSpeedLimitInMetersPerSecond ) == static_cast< bool >( rhs.optimalWeatherSpeedLimitInMetersPerSecond ) ) ) &&
            ( ( schoolZoneSpeedLimitInMetersPerSecond && rhs.schoolZoneSpeedLimitInMetersPerSecond )
                ? ( *schoolZoneSpeedLimitInMetersPerSecond == *rhs.schoolZoneSpeedLimitInMetersPerSecond )
                : ( static_cast< bool >( schoolZoneSpeedLimitInMetersPerSecond ) == static_cast< bool >( rhs.schoolZoneSpeedLimitInMetersPerSecond ) ) ) &&
            ( ( timeDependentSpeedLimitInMetersPerSecond && rhs.timeDependentSpeedLimitInMetersPerSecond )
                ? ( *timeDependentSpeedLimitInMetersPerSecond == *rhs.timeDependentSpeedLimitInMetersPerSecond )
                : ( static_cast< bool >( timeDependentSpeedLimitInMetersPerSecond ) == static_cast< bool >( rhs.timeDependentSpeedLimitInMetersPerSecond ) ) );
}

Note that when checking the equality of Double values, it looks for exact equality! Instead, for the Float and Double properties, we should utilize an epsilon value when two such properties are compared for equality. Ideally, Float and Double should utilize two default epsilon values which are defaulted to some values when not specified at all and let each property specify its custom epsilon.

@Equatable
@DoubleEpsilon(0.01)   # Default epsilon value for the Double's
@FloatEpsilon(0.05)    # Default epsilon value for the Float's
struct SpeedLimit {
    . . .
    @DoubleEpsilon(0.001) # Custom epsilon value for this Double property, 
                          # it will generate a warning (or error?) if the parent 
                          # class/struct is not @Equatable
    advisory_speed_limit_in_meters_per_second: Double? = null
    . . .
}

OguzHere avatar Apr 27 '22 14:04 OguzHere

I'd propose to simplify the custom epsilons to just one @Epsilon() attribute, with the following semantics:

  • applicable per-field only, not on struct level (to avoid the need to distinguish Float and Double)
  • specifying the epsilon in a non-@Equatable struct does nothing (that's the general approach for attributes we have)
  • a Float or Double field with no @Epsilon works as before (i.e. direct comparison). This avoids the need for a hard-coded default epsilon. Also gives the IDL author full control over the feature.

DanielKamkha avatar Apr 27 '22 14:04 DanielKamkha

I agree with your simplification.

OguzHere avatar Apr 27 '22 14:04 OguzHere

Using epsilon on equality breaks A = B -> A.hash = B.hash invariants.

derolf avatar May 01 '22 22:05 derolf