Macaulay2 / M2

The primary source code repository for Macaulay2, a system for computing in commutative algebra, algebraic geometry and related fields.
https://macaulay2.com
330 stars 226 forks source link

Make Constant and InfiniteNumber subclasses of Number #3318

Closed d-torrance closed 6 days ago

d-torrance commented 1 week ago

Currently, we can create subclasses of Number:

i1 : MyNumberType = new Type of Number

o1 = MyNumberType

o1 : Type

But we can't actually create any instances of these types:

i2 : x = new MyNumberType from {}
stdio:2:5:(3): error: 'new' expected a type of list or hash table

We modify things so that we can create subclasses of Number whose instances are lists or hash tables:

i2 : x = new MyNumberType from {}

o2 = <<a list>>

o2 : MyNumberType

Such objects will forget that they are lists or hash tables (except for low-level things like accessing values with #), but provided that we install a numeric method, then most Number methods should work out of the box:

i3 : numeric MyNumberType := x -> 0.0

o3 = FunctionClosure[stdio:3:26-3:29]

o3 : FunctionClosure

i4 : sin x

o4 = 0

o4 : RR (of precision 53)

The reason to do all of this is to simplify how we define methods for the Constant type, which is currently a subclass of BasicList for things like pi and ii. By making Constant a subclass of Number, we can use inheritance and not have to worry about installing a ton of methods to make sure that things like sin pi will work.

Before

i1 : methods regularizedBeta

o1 = {0 => (regularizedBeta, Constant, Constant, Constant)}
     {1 => (regularizedBeta, Constant, Constant, Number)  }
     {2 => (regularizedBeta, Constant, Constant, RR)      }
     {3 => (regularizedBeta, Constant, Number, Constant)  }
     {4 => (regularizedBeta, Constant, Number, Number)    }
     {5 => (regularizedBeta, Constant, Number, RR)        }
     {6 => (regularizedBeta, Constant, RR, Constant)      }
     {7 => (regularizedBeta, Constant, RR, Number)        }
     {8 => (regularizedBeta, Constant, RR, RR)            }
     {9 => (regularizedBeta, Number, Constant, Constant)  }
     {10 => (regularizedBeta, Number, Constant, Number)   }
     {11 => (regularizedBeta, Number, Constant, RR)       }
     {12 => (regularizedBeta, Number, Number, Constant)   }
     {13 => (regularizedBeta, Number, Number, Number)     }
     {14 => (regularizedBeta, Number, Number, RR)         }
     {15 => (regularizedBeta, Number, RR, Constant)       }
     {16 => (regularizedBeta, Number, RR, Number)         }
     {17 => (regularizedBeta, Number, RR, RR)             }
     {18 => (regularizedBeta, RR, Constant, Constant)     }
     {19 => (regularizedBeta, RR, Constant, Number)       }
     {20 => (regularizedBeta, RR, Constant, RR)           }
     {21 => (regularizedBeta, RR, Number, Constant)       }
     {22 => (regularizedBeta, RR, Number, Number)         }
     {23 => (regularizedBeta, RR, Number, RR)             }
     {24 => (regularizedBeta, RR, RR, Constant)           }
     {25 => (regularizedBeta, RR, RR, Number)             }
     {26 => (regularizedBeta, RR, RR, RR)                 }

o1 : NumberedVerticalList

After

i1 : methods regularizedBeta

o1 = {0 => (regularizedBeta, Number, Number, Number)}
     {1 => (regularizedBeta, Number, Number, RR)    }
     {2 => (regularizedBeta, Number, RR, Number)    }
     {3 => (regularizedBeta, Number, RR, RR)        }
     {4 => (regularizedBeta, RR, Number, Number)    }
     {5 => (regularizedBeta, RR, Number, RR)        }
     {6 => (regularizedBeta, RR, RR, Number)        }
     {7 => (regularizedBeta, RR, RR, RR)            }

o1 : NumberedVerticalList
mahrud commented 1 week ago

Can we also have InfiniteNumber be a subclass? Then in functions that accept infinity we need to check that the only allowed Number is +/-infinity

d-torrance commented 1 week ago

Done! Now things like atan infinity work.

pzinn commented 1 week ago

I'm all in favour of such a change. I do feel like this is a bit hacky though. Until now, one of the basic principles of the class system of M2 is that only BasicList and HashTable have meaningful inheritance trees, in the sense that user-created classes necessarily have the same data type as its ancestor. If we're going to break this principle, maybe we should do it in a more systematic way? BTW this also reminds me of the RingElement vs Number debate (don't have a comp atm so can't look up the relevant link).

d-torrance commented 1 week ago

I guess I'm proposing that we add Number to BasicList and HashTable as possible ancestors of user-defined classes. :) (That list isn't complete either -- users can also subclass FunctionClosure and PythonObject. )

Right now, the subclasses of Number are a hodge-podge of different wrappers around various C structs from GMP, MPFR, and MPFI. The actual implementations don't matter, but what's important is that we can perform arithmetic operations on them, plug them into functions, etc. In this sense, Constant and InfiniteNumber are the same. Sure, under the hood, they're lists, but we'll never actually want to do anything list-like with them other than access their elements so we can get the information we need to do number-like things with them.

d-torrance commented 1 week ago

Here's the Number/RingElement discussion: #1519

d-torrance commented 1 week ago

I went ahead and made IndeterminateNumber a subclass of Number for good measure.

mahrud commented 1 week ago

There's only InexactNumber left!

d-torrance commented 1 week ago

It was already good to go (it's defined in classes.dd):

i1 : ancestors InexactNumber

o1 = {InexactNumber, Number, Thing}

o1 : List
mahrud commented 1 week ago

Ah I see! In response to Paul's point, I'd like to repeat the suggestion of having UnionTypes from https://github.com/Macaulay2/M2/issues/1979. It would make a lot of method declarations easier.

mahrud commented 1 week ago

Wait, I thought the idea was for Number to be the type family for all types of numbers, some implemented as lists and some not. Why add an intermediary?

d-torrance commented 1 week ago

I realized I was needing to define peek and toExternalString for each of these top-level Number types since they were losing their inheritance on BasicList. I figured adding an intermediate type would simplify this. Also, this lets us use all the Constant arithmetic methods that use numeric for any other top-level number types. InfiniteNumber already had its own, but now things like the following work:

i1 : 2 + indeterminate

o1 = NotANumber

o1 : RR (of precision 53)
mahrud commented 1 week ago

Sorry, I don't follow. What's an example of this problem? I think the name ListNumber is misleading.

d-torrance commented 1 week ago

The main issue is that there were a few methods that no longer worked for these types since they no longer inherited from BasicList that we might want to keep around. toExternalString was the big one. It was calling simpleToString and returning <<a list>>, which resulted in a syntax error building the documentation. I figured peek was also nice to have. There might be others.

I figured a parent class for all Number types that are implemented using lists would be nice to simplify installing these sorts of methods. So rather than:

toExternalString Constant := toExternalString InfiniteNumber := toExternalString IndeterminateNumber := lookup(toExternalString, BasicList)

we can do:

toExternalString ListNumber := lookup(toExternalString, BasicList)

I'm definitely open to changing the name.

mahrud commented 1 week ago
  1. I don't think the previous toExternalString methods were correct to begin with, and your change highlighted that problem. For instance, this is so unnecessary and bizarre:
    
    i7 : toExternalString(-infinity)

o7 = new InfiniteNumber from {-1}

2. Further proving the point, I think this is a regression:
```m2
i12 : toExternalString pi

o12 = new Constant from {symbol pi,pi0,piRRi0}
  1. I think each of these types should have their own specialized methods and not one inherited from a common ancestor. This will keep flexibility for future types, like p-adic numbers implemented as a tuple, which is technically a ListNumber as well, but very different from the others.
  2. I'm a little confused by the goal of this last commit. Is this related to implementing Number as a TypeFamily or UnionType? If so, I think we should do it properly, defining those types and making the necessary changes in the interpreter. Dan's old suggestion was that installing a method on something like Number = new TypeFamily from {Constant, InfiniteNumber, ...} would go through and install the method on each member one by one. I think this is fine for now, but in the long term my hope is something like
    Number = new UnionType
    Constant = new Type of Number from BasicList

    Then Number would be an interface-only type, and Constant would be a type of BasicList. Then lookup(func, Constant) would first check (func, Number) and its ancestors, then (func, BasicList) and its ancestors.

(sidenote: I think new Module of Vector from {...} should really be new Vector of Module from {...}).

d-torrance commented 1 week ago

Fair enough -- I'll revert the last commit.