NonPromoting.jl

Non-promoting wrappers for arithmetic types
Author eschnett
Popularity
2 Stars
Updated Last
3 Years Ago
Started In
November 2018

A Julia library for non-promoting type wrappers, i.e. for arithmetic types that do not get automatically promoted to other types. This can help prevent subtle type errors

Build Status (Travis) Build status (Appveyor) Coverage Status (Coveralls)

Overview

Sometimes, Julia's automatic type promotion gets things subtly wrong. This is especially a problem if you want a precision higher than Float64, since Float64 is often the default go-to type when a floating point number is generated from non-floating-point numbers.

Here is an example. Of course, this code is so simple that one can spot the problem right away. If multiple modules are involved, then spotting this isn't that easy any more.

function pi_plus_tenth_bad()
    x = big(pi)   # highly accurate
    y = 1/10      # unintentionally inaccurate
    x + y
end

The problem here is that the expression 1/10 has type Float64, which is much less accurate than BigFloat. The error is difficult to spot since the type Float64 is not mentioned explicitly. Float64 just happens to be the default type that Julia chooses when dividing two integers.

This version of the code does not have a problem:

function pi_plus_tenth()
    x = big(pi)   # highly accurate
    y = 1//10     # infinitely accurate
    x + y
end

This uses a rational number to represent the fraction 1/10, which is arbitrarily accurate when converted to BigFloat.

One way to avoid such surprises is by avoiding automatic type promotion. If the compiler refuses the expression +(::BigFloat, ::Float64), then the error is quickly detected.

Design

The module NonPromoting defines a wrapper type NP{T}. T is expected to be a subtype of AbstractFloat such as Float64, BigFloat, etc. This type supports all operations that T supports, except that the automatic promotion rules to and from T are disabled. Julia is very efficient for this kind of setup, and there is zero run-time overhead.

To make a value non-promotable, call the constructor NP{T}. To extract the value, convert it back to T with convert(T, x). The usual arithmetic operations (add, subtract, multiply, square root, trigonometric functions, ...) are supported directly on the NP{T} type.

The first (inaccurate) code looks like this:

using NonPromoting
function pi_plus_tenth_bad()
    x = NP(big(pi))   # highly accurate
    y = NP(1/10)      # unintentionally inaccurate
    x + y             # ERROR REPORTED HERE
end

This code will now produce an error, because the addition +(::NP{BigFloat}, NP{Float64}) is not defined, and Julia will not promote NP{Float64} to NP{BigFloat}.

This version is explicit about types and works correctly (i.e. accurately):

using NonPromoting
function pi_plus_tenth()
    x = NP{BigFloat}(pi)      # highly accurate
    y = NP{BigFloat}(1//10)   # also highly accurate
    x + y
end

The basic design idea is that one uses the type NP{T} instead of T everywhere, except to interface with other modules who do not know about NonPromoting.