How about support for rational numbers in Elixir/BEAM?


there’s already an awesome Ratio library by @Qqwy, but it has several problems that, I believe, can’t be solved in the current state of the language:


Ratio provides support for overloading standard operators to handle addition, multiplication, etc and also comparison. The problem is, when someone forgets to do use Ratio, the comparison won’t fail, but it will return invalid results… from time to time. That’s because comparing any Elixir terms is valid (btw this problem made me wonder if it should be). I experienced that a couple of times already and believe me, it’s extremely hard to debug.


Ratio operations are more resource-consuming than build-in numbers because we have to extract two integers from a struct each time. What’s more, we often have to find the GCD of these integers to avoid them growing indefinitely. Having that implemented natively surely would speed it up. However, maybe a bigger problem is that since Ratio overloads operators, it adds overhead in all the places when they’re used, even those not involving any rationals. Disclaimer: didn’t make any benchmarks yet, will do shortly. However, it’s rather a matter of the scale of the slowdown, not its existence.

Code reuse

Of course, whatever operates on rationals provided by Ratio has to be aware of them and rely on Ratio. That makes it impossible to reuse existing code that doesn’t have that dependency if they do any operations on numbers.

Supporting rationals can also be seen as a natural expansion of having big integers to floats - big integers prevent integer overflows, while rationals prevent error accumulation. And since rationals consist of integers, we get the benefits of both :wink: It would be a game-changer for the ones who use rationals all over the place - as we do in Membrane :wink:

Got a little confused since Ratio 3.x no longer supports use Ratio. In general, I don’t like operator overloading neither in this case, nor in my times with C++. I think this should be approached the same as comparing DateTimes.

I think I’d like the addition of rationals into the VM if we had some sort of static typing. Knowing types at compile time allows the VM to squeeze out more performance. Supposedly it would be also visible to the developer that a particular “a + b” may be more time consuming then adding two integers.

1 Like

Yup, we didn’t yet update to Ratio 3.0, because we use it in many places and we’d have to do it all at once, so it’s not just a matter of bumping the version :wink: I generally agree that operator overloading is confusing when applied to things like dates, but when it comes to numbers, well, these operators were created for handling numbers. Not using operators makes things twice as less readable and adds the risk that someone accidentally uses > instead of Regarding performance, I don’t think that the difference between integers and rationals would be that crucial to make it always visible for developers. They are not tensors after all :wink: And the compiler still has to check for integers and floats upon each operation.

I feel like the decision against operator overloading has been made a long time ago. Otherwise I expect us to have operator overloading for the Decimal library, which has been around since around elixir 0.13. I’d be curious if an approach like Nx took with defn would be more fruitful, where the mathematical operations can be defined in a context separate to core elixir, but using the same syntax.

Even with operator overloading like Numbers does, you still can’t just naively reuse code that was expecting number() values, since comparisons in guards aren’t overloaded:

def foo(x) when x > 0 do
  x * 3
def foo(x) do
  x * 2

# vs

def foo_no_guard(x)
  if x > 0 do
    x * 3
    x * 2

Nice to hear that you are using Ratio! :blush:

Unfortunately, Elixir’s hands are a bit tied in this regard: It is impossible to add support for a rational type in a way that would work as expected in guards without changing the Erlang VM itself.

Personally I would be very happy if support for decimals and rationals would be added as builtins to Erlang, because I also think that their usage is common enough to warrant this. However, I expect that such a change is highly unlikely because it would be a large undertaking to add support to them. (This besides the fact that not everyone agrees that they should be added in the first place.)

To respond to your separate points in detail:

  • Comparison: This is the case to allow collections (like maps and sets) to work with any mixture of keys/elements. In practice these kinds of collections are rather rare, however.
  • Performance: I have not done any benchmarking myself. I’d expect the code to be reasonably performant now that we have a JIT, but it would definitely be interesting to look at how fast things would be if one were to inline most of the logic in the final module; currently there are multiple function-call-indirections that happen for many operations. But long story short: If you are doing a lot of math that needs to be performant, you might want to look into using Nx or a NIF.
  • Reuse: Being able to support arithmetic for arbitrary datastructures is the gap the Numbers tries to fill. If you’re using a dependency that does not allow it, you could always contribute a PR to that dependency. In my experience, people are quite open to this kind of extension of functionality :blush: .

Thanks for the answer and the library :slight_smile:

Concerning guards, maybe it would be possible to support them? Now it’s possible to extract values from the struct and I think it wouldn’t be necessary to reduce the fractions there :thinking:

I’m afraid I agree. I’ll ask at the Erlang forum though, maybe they have some useful thoughts.

We don’t do any big math in Elixir, though small operations done frequently in different places contribute to the load. Since they’re small, I don’t expect delegating to the native code to help. But I’ll come back when I have benchmarks :wink:

Good to hear that Numbers are welcome :wink: