SafeInt - Safe 54-bit signed integer

I’ve just published my first Elm package: malaire/elm-safe-int

SafeInt

SafeInt is 54-bit signed integer which works for full JavaScript safe integer range from - (2^53 - 1) to 2^53 - 1 (from -9007199254740991 to 9007199254740991).

-- `2^40 / 10`
SafeInt.pow SafeInt.two (SafeInt.new 40)
    |> SafeInt.divBy (SafeInt.new 10)
    |> SafeInt.toInt
    --> Just 109951162777

SafeInt has a concept of undefined: When function result can’t be represented using an integer within safe integer range, result will be undefined which is propagated through all functions:

-- `12 / 0 * 34`
-- undefined because of division by zero
SafeInt.div (SafeInt.new 12) SafeInt.zero
    |> SafeInt.mul (SafeInt.new 34)
    |> SafeInt.toInt
    --> Nothing

-- `2 ^ 55 / 1000`
-- undefined because safe integer range is exceeded
SafeInt.pow SafeInt.two (SafeInt.new 55)
    |> SafeInt.divBy (SafeInt.new 1000)
    |> SafeInt.toInt
    --> Nothing

SafeInt.Unchecked

For cases where more speed is required at the cost of some lost safety and features, module SafeInt.Unchecked provides Unchecked versions of SafeInt functions which work directly on Float values.

These are still integer functions and e.g. divBy does integer division:

import SafeInt.Unchecked as Unchecked

-- `2^40 / 10`
Unchecked.pow 2 40
    |> Unchecked.divBy 10
    --> 109951162777

Performance

I’m not interested in doing proper benchmarking, but in some quick benchmarks:

  • SafeInt was up to 6 times slower than Int or Float
  • Unchecked was up to 3 times slower than Int or Float
  • SafeInt was up to 2.5 times slower than Unchecked

Too many divisions

Ever wanted a package with 24* basic integer-division-related functions? Probably not, but here you go. :grin:

import SafeInt.Unchecked as Unchecked

Unchecked.divMod -1234 100
    --> ( -13, 66 )

-1234 |> Unchecked.quotRemBy 100
    --> ( -12, -34 )

*) div, mod, quotient, remainder, divMod, quotRem, then By versions and SafeInt / Unchecked versions.

Note: Using flip or even defining a function as modBy a b = mod b a adds significant overhead on small functions like these.

When Int is not an integer

Did you know that in Elm Int can be NaN, -Infinity, +Infinity, or even a floating-point value like 1234.5 ?

But don’t worry, SafeInt handles all of these cases - it wouldn’t be Safe if it didn’t. For example SafeInt.fromInt : Int -> SafeInt is described as “Convert Int to SafeInt, rounding towards zero”, so if you give it a floating-point value as Int, it will be properly truncated to an integer. And NaN or infinities will be converted to undefined.

SafeInt.pow also truncates the result, always returning either an integer or undefined, unlike ^ operator:

$ elm repl
> round 2 ^ -1
0.5 : Int
> import SafeInt
> SafeInt.pow SafeInt.two (SafeInt.new -1)
Defined 0 : SafeInt.SafeInt

And SafeInt functions of course never crash, unlike Int functions.

More about design goals in README.

8 Likes

Very nice. Do you think an identical API is possible with safe 64-bit integers?

Reason I ask is I’ve worked with a few back-end APIs recently that provided values that could take a full 64-bit range. These were not just identifiers, they were scalar values.

There is folkertdev/elm-int64 for some 64-bit operations, but it doesn’t implement division or multiplication which are tricky to get correct.

To implement e.g. 64-bit multiplication, I think it would be necessary to split 64-bit integer into three parts each at most 26 or 27 bits, as then result of each individual multiplication would fit within safe 54 bit range.

Calculations would include quite many individual steps, and would be much slower than with 54-bit integer which is basically just a single Float operation (with many additional checks in case of SafeInt).

4 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.