I'm trying to rewrite my Nemo package to make it conform as much as possible to the conventions of the Julia language.
On that score I finally relented and decided to stick with the Julia convention of using // to construct fractions, instead of /, and to stick with the (to me) strange definition of div for values of type BigInt{Rational} (historical note: it turns out this is the original definition Euclid used, despite the fact that modern algebraists do not define it that way in a "Euclidean domain", such as the field of rational numbers). I also decided to simply learn to get used to the inconsistent semantics of integer literals as either Int64's, Int128's or BigInt's depending on their size. I want Nemo to be as consistent with the rest of Julia, and its design intentions as possible, even though my application for the language is somehow quite different.
[[ Nemo is a computer algebra package for Julia, which allows constructing various Rings (e.g. polynomial rings, residue rings, finite fields, power series rings, p-adics, (and more recently number fields and order), recursively over each other). ]]
Of course I don't want to compromise on performance (at least, not much) or on neat code.
After reworking a substantial proportion of the Nemo codebase, I've resolved most of the problems, but Rational{BigInt} is currently causing me substantial difficulties which I've been unable to resolve.
In particular, I want Nemo to make use of the // notation to construct fractions, as per the Julia convention. When using BigInt, this results in a Rational{BigInt} type.
I wonder if I could get some comment on the following issues:
1) The current Rational{BigInt} type appears to not be compatible with the GMP mpq_ type? It looks to me that effectively from a C interface perspective Rational{BigInt} is a type containing two pointers to mpz_structs. Whereas a GMP mpq_t contains two mpz_structs without the extra level of indirection.
Is this because Julia does not support embedding C structs within its types, and Julia wants Rational{BigInt} to consist of a pair of Julia types, namely BigInt's? Or am I mistaken about this.
This is problematic because I can't call GMP for the mpq_t functionality that I need, at least not without creating an auxiliary mpq type in Julia, copying the contents and adding substantial gc overhead. Or I can reimplement the mpq functions in Julia, which ends up being extremely inefficient due to gc, etc.
2) The Rational{BigInt} type appears to be immutable, which means that it can't be passed as the output to a GMP function (or any C function, for that matter) anyway. This adds overhead for all the same reasons.
3) I can't find a way to add Rational{BigInt} to my FieldElem typeclass in Nemo, as it already belongs to a typeclass (Real or something). This means I have to have T <: Union(Rational{BigInt}, FieldElem) everywhere throughout my code, and it becomes extremely unwieldy. Are there any plans in the pipeline to ease this "posthoc inheritance" issue? It currently makes interfaces hard to extend.
4) Because GC ends up taking most of the time for some computations in Nemo, we came up with some unsafe functions mul!, addeq!, etc., which do what you think they do, to cut down on the number of objects allocated by reusing objects for output. But Rational{BigInt} defeats this optimisation by being immutable. We can't write a mul! for Rational{BigInt}!
This optimisation is not just desirable, it is absolutely fundamental to our entire approach. And it is being defeated by immutability!
5) Things are a little worse for me in that the type Flint type uses for bignum rationals in an fmpq. So I need to convert to and from Rational{BigInt} for every operation involving the Julia Rational{BigInt} type, e.g. doing something with a coefficient of a rational polynomial. The following is currently the best hack I have come up with to handle this (note fmpq is the Flint rational type). I display it here just to show how dire these problems become:
# Not really an Int, but Julia doesn't provide an analogous type which is sometimes
# an Int and other times a pointer to an mpz_struct
type fmpz
data :: Int
fmpz() = new() # Sometimes we don't want flint to initialise, not Julia
end
function init(z::fmpz)
z.data = 0 # sometimes we do want to initialise in Julia
end
# Flint needs to clean up auxilliary data, such as mpz_t's it associates with
# its fmpz's, but can't use a finalizer, in case the fmpz is mocked up (see below)
function clear(z::fmpz)
ccall((:fmpz_clear, :libflint), Void, (Ptr{fmpz},), &z)
end
# We want to pass a Julia BigInt to Flint, but they both use an mpz_t for integers
# that don't fit in an Int, and we definitely don't want to copy the data if we only
# want to read it. So we mock up an fmpz for flint. Note Julia must not call the
# fmpz_clear function on a mocked up fmpz, hence the explicit init/clear functions
# above instead of a finalizer.
function fmpz_readonly(x::BigInt)
temp = fmpz()
ccall((:fmpz_init_set_readonly, :libflint), Void, (Ptr{fmpz}, Ptr{BigInt}), &temp, &x)
return temp
end
# Convert back to a Julia BigInt
function BigInt(z::fmpz)
r = BigInt()
ccall((:fmpz_get_mpz, :libflint), Void, (Ptr{BigInt}, Ptr{fmpz}), &r, &z)
return r
end
type fmpq
num::Int # not really an Int, but Julia doesn't provide an analogous type
den::Int # not really an Int, but Julia doesn't provide an analogous type
fmpq() = new() # we only sometimes want to initialise the values in Julia itself
end
function init(z::fmpq)
z.num = 0 # sometimes we do want to initialise in Julia
z.den = 1
end
function clear(z::fmpq)
ccall((:fmpq_clear, :libflint), Void, (Ptr{fmpq},), &z) # flint needs to clean up after use
end
# try to avoid copying all the data, since Flint and Julia both use mpz_t's for the
# components of their rational types, and we only want to *read* the values
function fmpq_readonly(x::Rational{BigInt})
temp = fmpq()
temp2 = fmpz()
ccall((:fmpz_init_set_readonly, :libflint), Void, (Ptr{fmpz}, Ptr{BigInt}), &temp2, &x.num)
temp.num = temp2.data
ccall((:fmpz_init_set_readonly, :libflint), Void, (Ptr{fmpz}, Ptr{BigInt}), &temp2, &x.den)
temp.den = temp2.data
return temp
end
# We need to create an actual Julia Rational{BigInt} type to convert from Flint
# fmpq back to Julia. But copying the data is currently extremely problematic.
function Rational(z::fmpq)
temp = fmpz()
temp.data = z.num
a = BigInt(temp)
temp.data = z.den
b = BigInt(temp)
return Rational(a, b)
end
# This is an absolute mess. It should be a single call to a GMP mpq function or a
# simple efficient readonly conversion to flint types, a call to a flint function and
# a simple conversion back to Julia types (saving that final copy is probably not
# possible.
function gcd(a::Rational{BigInt}, b::Rational{BigInt})
temp1 = fmpq_readonly(a)
temp2 = fmpq_readonly(b)
temp3 = fmpq()
init(temp3)
ccall((:fmpq_gcd, :libflint), Void, (Ptr{fmpq}, Ptr{fmpq}, Ptr{fmpq}), &temp3, &temp1, &temp2)
r = Rational(temp3)
clear(temp3)
return r
end
This is incredibly painful and inefficient. And yes, I need all of that because of the different things I need to do with fmpz's and fmpq's. This is just a single example. The semantics needs to be the same as the rest of Nemo because of the recursive nature of the data structures in Nemo, so an individual hack is not useful, or we'd need to do the same thing right across Nemo.
I've thought about not using either Julia BigInt's or Rational{BigInt}'s in Nemo as the default bignum types.
But note that I cannot just make the BigInt type in Nemo to be an fmpz (the Flint integer type) since otherwise one has to type ZZ(123.....145) or fmpz(123......145) or something similar to enter BigNums instead of 123......145 (extremely painful for polynomials with large coefficients -- they can currently be cut-n-pasted from other systems, Magma, Sage, Pari, etc., modulo the use of // instead of / for constructing rationals of course), and any BigNum features of Julia would then require back and forth by the user to convert between the two types. I want to make Nemo as Julian as possible and to be able to interface with other packages as easily as possible!
I also cannot redefine the // operator to construct an fmpq instead of a Rational{BigInt} from BigInt's because apparently redefining operators breaks them for everyone else (i.e. all other packages that are subsequently loaded or currently loaded). I want Nemo users to maximally benefit from other packages available for Julia. (For example I tried redefining / for Int's and it broke the Julia profiler!!)
If Julia intends to use GMP mpz_t for BigInt, I **strongly** believe the Rational{BigInt} type should be binary compatible with the GMP mpq_t, and it should be a mutable type.
I realise the whole bignum thing in Julia needs reworking, and this is not intended to be a finalised implementation. But as things currently are, it is causing unnecessary difficulties for me. As it is, any potential solution I come up with causes me to lose something. I either loose:
1) The ability to use integer literals to enter BigInts,
2) The ability to use the // operator to construct Rational{BigInt}'s; or
3) I end up with a lot of extremely ugly, inefficient code that does lots of copying.
I'm really trying hard to stick with Julian conventions as much as possible, but the current implementation makes this incredibly hard.
I welcome any comments/suggestions.
One other suggestion I could make is that modules by default use the definition of operators from Base, unless they are redefined inside that module or a module that is imported by that module. This would include the REPL user.
This would allow Nemo to redefine // for BigInt. Users of Nemo would have it redefined for them, including someone using Nemo from the REPL, but no one else would have the operators redefined. In particular, if a module internally uses //, it would use the Base definition, even if the user was using Nemo.
Somehow, I thought this was the case. But I broke the profiler doing this.
I realise most discussion occurs on GitHub tickets. But if possible I would like to first discuss the totality of the issues, not individual issues which isolated, could get closed as invalid or wontfix. Of course I am happy to open individual tickets later once I understand what the current thinking is.
Bill.
P.S: I am prepared to help implement something better for BigInt and Rational{BigInt}. One of my areas of expertise is efficient bignum implementation.