Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce new numerics #200

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
230 changes: 230 additions & 0 deletions text/0000-expand-math.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
- Feature Name: Introduce new numerics -- `Rational`, `BigInt`, `BigFloat`, `Complex`
- Start Date: 2022-02-28
- RFC PR:
- Pony Issue:

# Summary

This RFC proposes the introduction of new numeric types; in particular the addition of a type representing a fractional number (`Rational`), arbitrary precision integer (`BigInt`), arbitrary precision float (`BigFloat`), and complex number (`Complex`).

# Motivation

The primary motivation for adding these types to the stdlib is to have a single canonical implementation of them which allow interoperability of numeric types across the Pony ecosystem.

# Detailed design

I propose we add the aforementioned numeric types into `builtin` so they exist alongside the other standard numeric types. These introduced numeric types **must** existing within the current numeric type hierarchy by being compliant with existing numeric traits.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, the preference would be to keep builtin as minimal as possible, so I'd prefer to put these new types in a new package (or series of packages), unless there is a strong motivation for it to go in builtin.

Pretty much all of the existing public types in builtin have hard requirements forcing them to be there, with a reason like one of the following:

  • they are used by core language constructs (None, string literals, numeric literals, etc)
  • they need to use raw pointers (Array, String, etc)
  • they use compile_intrinsic for one or more function definitions
  • they are part of the Env, which is passed to the Main actor on entry
  • they are used by one of the types that meet the above criteria

I don't think these new types have any hard requirements forcing them to go in builtin, so I believe they shouldn't be there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can discuss this further on Sync and I will update the RFC according to our conversation. I have no particular need for these to be added to builtin so no objection to an agreed upon other location such as a newly created numerics or adding these to math (as was the RFC state prior to my latest changes).


## Numeric Hierarchy

Current the Pony numeric type hierarchy is as follows:

```mermaid
classDiagram
class Any
<<interface>> Any
class Real
<<trait>> Real
class FloatingPoint
<<trait>> FloatingPoint
class Integer
<<trait>> Integer
class SignedInteger
<<trait>> SignedInteger
class UnsignedInteger
<<trait>> UnsignedInteger

Any <-- Real
Real <-- FloatingPoint
Real <-- Integer
Integer <-- SignedInteger
Integer <-- UnsignedInteger

FloatingPoint <-- F32
FloatingPoint <-- F64

UnsignedInteger <-- U8
UnsignedInteger <-- U16
UnsignedInteger <-- U32
UnsignedInteger <-- U64
UnsignedInteger <-- U128
UnsignedInteger <-- USize
UnsignedInteger <-- ULong

SignedInteger <-- I8
SignedInteger <-- I16
SignedInteger <-- I32
SignedInteger <-- I64
SignedInteger <-- I128
SignedInteger <-- ISize
SignedInteger <-- ILong
```

This RFC introduces four more numeric types: `Rational`, `BigInt`, `BigFloat`, and `Complex`. These fit into the numeric type hierarchy in the following manner:

```mermaid
classDiagram
class Any
<<interface>> Any
class Real
<<trait>> Real
class FloatingPoint
<<trait>> FloatingPoint
class Integer
<<trait>> Integer
class SignedInteger
<<trait>> SignedInteger
class UnsignedInteger
<<trait>> UnsignedInteger

Any <-- Real
Any <-- Complex~Real~
Real <-- FloatingPoint
Real <-- Integer
Real <-- Rational~Integer~
Integer <-- SignedInteger
Integer <-- UnsignedInteger
Integer <-- BigInt

FloatingPoint <-- F32
FloatingPoint <-- F64
FloatingPoint <-- BigFloat

UnsignedInteger <-- U8
UnsignedInteger <-- U16
UnsignedInteger <-- U32
UnsignedInteger <-- U64
UnsignedInteger <-- U128
UnsignedInteger <-- USize
UnsignedInteger <-- ULong

SignedInteger <-- I8
SignedInteger <-- I16
SignedInteger <-- I32
SignedInteger <-- I64
SignedInteger <-- I128
SignedInteger <-- ISize
SignedInteger <-- ILong
```

## Methods of Concern

```pony
trait val Real[A: Real[A] val] is
(Stringable & _ArithmeticConvertible & Comparable[A])
...
new val min_value()
new val max_value()
...
```
Copy link

@jasoncarr0 jasoncarr0 Mar 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that this has already been present, but given that this is re-design I'm not clear on the benefit of these constructors, but there's some obvious costs. Is there a use case for max_value() / min_value() that makes sense for all numbers? Given that this doesn't work for BigInt/BigFloat/similar types I'd be hesitant to place it at the top of the hierarchy.

Can we instead make a Bounded interface? This will only break code which relies on these methods being present in any Real[A]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you are suggesting. What's a "bounded interface"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think he means a new interface whose name is Bounded, which has the min and max value methods on it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasoncarr0 is that what you meant? re: Bounded interface? if yes, did you specifically mean structural typing or were you using "interface" more loosely?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant a literal Pony interface, that is:
These constructors would be part of an interface named Bounded, while the other methods would be part of this trait.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually there is also an argument for removing Ordered, but only because this trait is really the lowest possible. Mostly that the implementations for things like matrices would be sketch but making them numerics makes sense and is powerful.


`Real` will exist above `Rational`, `BigInt`, and `BigFloat` and as such would require defining the above methods for these types. `Rational` can be defined as minimum and maximum of the numerator, however `BigInt` and `BigFloat` by definition have arbitrary precision making defining a minimum and maximum difficult at the least -- if we define them as the minimum and maximum of a machine-sized int and float, or define them as -Inf and Inf -- or impossible at the worst -- if we define them by their possible limits which are arbitrary.

```pony
trait val Integer[A: Integer[A] val] is Real[A]
...
fun op_and(y: A): A => this and y
fun op_or(y: A): A => this or y
fun op_xor(y: A): A => this xor y
fun op_not(): A => not this

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't be implemented by a BigInt, so it's odd that a BigInt can't actually be an Integer

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't understand your comment. Can you try explaining in a different way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't do a bitwise "not" operation on a BigInt - at least not by the normal semantics of a bitwise "not" operation.

You can't because the you'd expect that any bits "more significant" than the most significant bit of the value would be set to 1.

But a BigInt has no upper bound, and thus there is no limit to the number of leading 1 bits implied by inverting its bits

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand @jemc.

"This can't be implemented by a BigInt, so it's odd that a BigInt can't actually be an Integer"

Can you explain how what you said is related to Jason's comment?

I don't understand why not being able to be implemented BigInt makes it odd that BigInt can't actually be an Integer.
I don't understand what is being said here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll copy Jason's words and insert parentheticals with my own commentary:

This (the op_not function) can't be implemented by a BigInt (for the reason mentioned in my previous comment)

So it's odd that a BigInt (which is conceptually a kind of integer) can't actually be an Integer (it cannot be a subtype of the Integer trait, because the Integer trait includes the op_not function).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jemc. I understand now.


fun bit_reverse(): A
"""
Reverse the order of the bits within the integer.
For example, 0b11101101 (237) would return 0b10110111 (183).
"""

fun bswap(): A
```

`Integer` will exist above `BigInt` and as such would require defining the above methods -- however `BigInt` will be defined via other numerics so these methods could be applied recursively.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this means. What are "the above methods"? Those listed on the Integer trait?

"BigInt will be defined via other numerics so these methods could be applied recursively" <-- I don't understand what this means either. There seems to be something that I am supposed to understand about "other numerics" that BigInt will be defined via (did I miss that somewhere, if yes, bringing information forward so that you don't have to hold large chunks of RFC in your head would be good). I'm also not sure what "applied recursively" means in this context.


```pony
trait val FloatingPoint[A: FloatingPoint[A] val] is Real[A]
new val min_normalised()
new val epsilon()
fun tag radix(): U8
fun tag precision2(): U8
fun tag precision10(): U8
fun tag min_exp2(): I16
fun tag min_exp10(): I16
fun tag max_exp2(): I16
fun tag max_exp10(): I16
...
fun abs(): A
fun ceil(): A
fun floor(): A
fun round(): A
fun trunc(): A

fun finite(): Bool
fun infinite(): Bool
fun nan(): Bool

fun ldexp(x: A, exponent: I32): A
fun frexp(): (A, U32)
fun log(): A
fun log2(): A
fun log10(): A
fun logb(): A

fun pow(y: A): A
fun powi(y: I32): A

fun sqrt(): A

fun sqrt_unsafe(): A
"""
Unsafe operation.
If this is negative, the result is undefined.
"""

fun cbrt(): A
fun exp(): A
fun exp2(): A

fun cos(): A
fun sin(): A
fun tan(): A

fun cosh(): A
fun sinh(): A
fun tanh(): A

fun acos(): A
fun asin(): A
fun atan(): A
fun atan2(y: A): A

fun acosh(): A
fun asinh(): A
fun atanh(): A
```

`FloatingPoint` will exist above `BigFloat` and as such would require defining the above methods -- many of which are ill-defined under arbitrary precision, or are functions using C-FFI and/or LLVM intrinsics.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused. Why would we do it this way if it has these problems? This seems like an argument against doing it this way. I feel like there's some meaning here that I am missing.


# How We Teach This

Adding ample documentation to these new numerics should suffice to teach Pony users how to leverage these types in their programs. I do not think any additions to the Pony Tutorial are needed, however if additions are desired than [Arithmetic](https://tutorial.ponylang.io/expressions/arithmetic.html) may be the most sensible location.

# How We Test This

I recommend use of `pony_check` to test all reversible operations pairs (`x+y-y == x`, `x*y/y == x`, etc), precision persistence (`Rational[U8](where numerator=x, denominator=y) * y == x`), and overflow/underflow protection (`Rational[U8](255, 1) + 1 => error`).

Testing these numerics should not affect any other parts of Pony and as such standard CI should suffice.

# Drawbacks

+ Additional maintenance cost
+ May break existing code if methods must be removed from existing numeric traits to match the suggested hierarchy placements

# Alternatives

Alternatively, we can introduce these types in `math` as opposed to `builtin` and/or only introduce some of the proposed new numeric types.

# Unresolved questions

+ Should these types be introduced in `builtin` or in `math`?
+ Does `Rational` make sense as the "fractional type" or would we prefer `Fractional` to avoid confusion?
+ Do we want to also include a `Decimal` type?
+ How should we handle the stated "Methods of Concern"?