Creating Guile bindings for MPFR, part 2

Precisions and rounding modes

In the first part of this mini-series we have gone through the motions of creating bindings from Guile to the floating point C library MPFR. As a running example, we have used IEEE 754 standard 113 bit quadruple precision. But the real goal of MPFR is to handle arbitrary (and in particular, arbitrarily large) precisions. Additionally, following the IEEE standard, it provides for different rounding modes, while so far we have limited ourselves to (admittedly the most common) rounding to nearest. This instalment treats these two aspects in a way that is idiomatic to Guile, provides flexibility and leaves programmers using guile-mpfr in control.

Choices of precisions

Precision handling can be achieved by setting a global default precision, as we did so far; after modifying the global parameter, all subsequent variables that are initialised by a call to mpfr_init will have this new precision. In a functional programming context such a recourse to state is not very idiomatic; more down to earth, it also poses questions in multithreaded code. In fact each MPFR variable carries its own precision, and the function mpfr_init2 takes as additional argument the precision at which a variable is initialised. This makes the precision explicit and should generally be preferred (the MPC library, written after MPFR, made the decision to completely drop the concept of a global precision and to only provide the function mpc_init2).

In MPFR, the variable that is supposed to hold the result of a computation is passed as an argument to the C function; in this way, also the desired target precision is passed along. In the functional context (but also when operators are overloaded, as in C++), results are instead returned as new objects, and choosing an appropriate precision becomes an issue with several possible solutions: One could use the global default precision regardless of the precision of the input values. One could use the maximum precision among all inputs with the hope of minimising precision losses. One could use the minimum precision among all inputs; implicitly this acknowledges that floating point inputs are already tainted by numerical imprecisions, that the last bit, say, may already be wrong, and that this error spills over into the result. One could explicitly consider all inputs as intervals with a given error (in the last bit, say), and use a precision that optimally approximates a result with the same semantics.

The choice made in guile-mpfr is to refuse any choice and to let the programmer remain responsible of precision handling as in the C library.

Handling precisions in the C wrapper library

In a first step, we modify guile_mpfr.c such that each C function returning an MPFR value takes as additional parameter the target precision. In C this would have the mpfr_prec_t type (usually a long); since we want to integrate the functions into the Guile REPL, the argument needs to be of SCM type, and we have to add an explicit conversion. So we add a new function scm_to_prec and modify the initialisation function mpfr_malloc to take an additional precision parameter, which is used to call mpfr_init2 instead of mpfr_init:

static mpfr_prec_t scm_to_prec (SCM prec)
{
   return (mpfr_prec_t) scm_to_long (prec);
}

static SCM mpfr_malloc (SCM prec)
{
   mpfr_ptr x;

   x = (mpfr_ptr) malloc (sizeof (__mpfr_struct));
   mpfr_init2 (x, scm_to_prec (prec));

   return scm_from_mpfrptr (x);
}

After that the conversion functions returning an MPFR value, as well as the macros wrapping computational functions, handle an additional parameter for the desired precision of the return value, which is passed through to mpfr_malloc; for instance:

SCM integer_to_mpfr (SCM val, SCM prec)
{
   SCM res;
   mpfr_ptr x;
   mpz_t z;

   res = mpfr_malloc (prec);
   x = scm_to_mpfrptr (res);
   mpz_init (z);
   scm_to_mpz (val, z);
   mpfr_set_z (x, z, MPFR_RNDN);
   mpz_clear (z);

   return res;
}

#define GUILE_MPFR_F_FF(name) \
SCM scm_mpfr_ ## name (SCM val1, SCM val2, SCM prec) \
{ \
   SCM res; \
   mpfr_ptr x1, x2, x; \
   res = mpfr_malloc (prec); \
   x = scm_to_mpfrptr (res); \
   x1 = scm_to_mpfrptr (val1); \
   x2 = scm_to_mpfrptr (val2); \
   mpfr_ ## name (x, x1, x2, MPFR_RNDN); \
   return res; \
}

For all these functions, we modify the library initialisation routine init_guile_mpfr by incrementing the number of arguments recorded through calls to scm_c_define_gsubr. In preparation of the following section, we also mark them as internal by prepending their names with an underscore:

void init_guile_mpfr (void)
{
   init_mpfr_type ();
   scm_c_define_gsubr ("_integer->mpfr", 2, 0, 0, integer_to_mpfr);
   scm_c_define_gsubr ("mpfr->double", 1, 0, 0, mpfr_to_double);
   scm_c_define_gsubr ("_mpfr-sin", 2, 0, 0, scm_mpfr_sin);
   scm_c_define_gsubr ("_mpfr-mul", 3, 0, 0, scm_mpfr_mul);
   scm_c_define_gsubr ("_mpfr-div", 3, 0, 0, scm_mpfr_div);
   scm_c_define_gsubr ("_mpfr-pi", 1, 0, 0, scm_mpfr_pi);
   scm_c_define_gsubr ("mpfr-print", 1, 0, 0, scm_mpfr_print);
}

The names of the exported functions at the Guile side are adapted accordingly. Again to prepare the following section, we add an indirection on the module level and record the changes in a new module mpfr-internal in the file mpfr-internal.scm:

(define-module (mpfr-internal)
  #:export (_integer->mpfr
            mpfr->double
            mpfr-print
            _mpfr-sin
            _mpfr-mul
            _mpfr-div
            _mpfr-pi))

(load-extension "guile_mpfr" "init_guile_mpfr")

The reason for the indirection is that load-extension should be the last line in a module. The loaded procedures can be used after that, but since they are loaded dynamically, the module compiler does not know their names and emits a warning about potentially unbound variables. Once the symbols are exported, however, they can be used without raising a warning. Since we do not want to export the symbols with an underscore to the outside world, we need to export them from an internal module that is supposed to not be imported into user code.

We can now do the same computations as in the previous post by starting a Guile REPL with

make
export GUILE_EXTENSIONS_PATH=.:$GUILE_EXTENSIONS_PATH
guile -L .
and executing
(use-modules (mpfr-internal))
(mpfr-print (_mpfr-mul (_integer->mpfr 2 53) (_mpfr-sin (_mpfr-div (_mpfr-pi 53) (_integer->mpfr 3 53) 53) 53) 53))
(mpfr-print (_mpfr-mul (_integer->mpfr 2 113) (_mpfr-sin (_mpfr-div (_mpfr-pi 113) (_integer->mpfr 3 113) 113) 113) 113))

to obtain the square root of 3 once in double and then in quadruple precision; the only difference to the previous code is that to each procedure call we add the precision at which it is carried out. The current state of the code is recorded in commit cf6da40 of the guile-mpfr project.

Parameterising the precision

Flexibility we have achieved, but convenience we have definitely lost. In a typical computation one may wish to change the precision from time to time (for instance when realising that the current precision is too low to obtain a satisfactory result), but usually, as in the example at the end of the previous section, whole swathes of computation are done at the same precision, and having to repeat the precision for each operation is cumbersome.

To solve this problem, we do something that at first sight looks like going back to the previously frowned upon solution of introducing a global variable; a variable, however, that will allow for local modification, namely a Guile parameter. On the surface a parameter is just a variable; more precisely it is a procedure that serves as a container for a value: If called without an argument, it returns the current value; if called with an argument, it stores the argument as the new current value. We add such a parameter to the (mpfr) module:

(define prec (make-parameter 53))

setting it to the default MPFR default precision. Then we can redefine all the procedures returning an MPFR value to take the current value of prec into account; for instance:

(define (mpfr-mul x y)
  (_mpfr-mul x y (prec)))

so that the (exported) procedure mpfr-mul calls the (hidden inside the mpfr-internal module) procedure _mpfr-mul with its own arguments, to which the current value of the precision parameter is added. This results in the following file mpfr.scm:

(define-module (mpfr)
  #:use-module (mpfr-internal)
  #:re-export (mpfr->double
               mpfr-print)
  #:export (integer->mpfr
            mpfr-sin
            mpfr-mul
            mpfr-div
            mpfr-pi
            prec))

(define prec (make-parameter 53))

(define (integer->mpfr x)
  (_integer->mpfr x (prec)))

(define (mpfr-sin x)
  (_mpfr-sin x (prec)))

(define (mpfr-mul x y)
  (_mpfr-mul x y (prec)))

(define (mpfr-div x y)
  (_mpfr-div x y (prec)))

(define (mpfr-pi)
  (_mpfr-pi (prec)))

Notice how the internal procedures are first imported from (mpfr-internal); the procedures not requiring a precision are then passed through by re-exporting them, while all others are exported under their final name. With this code, which is commit fef0e0c, the previous example becomes:

(use-modules (mpfr))
(mpfr-print (mpfr-mul (integer->mpfr 2) (mpfr-sin (mpfr-div (mpfr-pi) (integer->mpfr 3)))))
(prec 113)
(mpfr-print (mpfr-mul (integer->mpfr 2) (mpfr-sin (mpfr-div (mpfr-pi) (integer->mpfr 3)))))

Excursion: Guile macros

Again the transformation of underscored internal functions to exported functions follows the same pattern over and over again and depends only on the arity of the function, which calls for the use of macros. There are different solutions for this problem, and I thank Sergio Pastor Pérez and Ludovic Courtès for discussions and suggestions.

Coming from the C preprocessor, the following solution may feel natural. As in the C case with ##, we wish to programmatically concatenate tokens, respectively symbols in the Guile context, and fixed strings such as "_". This can be done conveniently by going back and forth between symbols and strings and doing the transformation on the strings. For instance, using define-macro we can write a macro for functions with two MPFR inputs and use it to define mpfr-mul and mpfr-div as follows:

(define-macro (private->public-2 f)
  (let ((private-f (string->symbol (string-append "_" (symbol->string f)))))
    `(define-public (,f x y)
      (,private-f x y (prec)))))

(private->public-2 mpfr-mul)
(private->public-2 mpfr-div)

The quasi-quoted define-public expression creates a nested list of symbols; it starts with 'define-public, then contains a list of three symbols, the macro argument f as a symbol such as 'mpfr_mul (here unquoting is needed since otherwise we would end up with the symbol 'f), 'x and 'y, and so on. Otherwise said, we end up with a quoted S-expression (or sexp) that holds the Scheme code we would otherwise write by hand. The calls to this macro are then evaluated in the current environment and create new public procedures mpfr-mul and mpfr-div. Since we have used define-public instead of define the procedures are also automatically exported from the module and can be dropped from the #:export clause of the module definition. Adding macros for the functions of other arities, we end up with the following complete module in mpfr.scm:

(define-module (mpfr)
  #:use-module (mpfr-internal)
  #:re-export (mpfr->double
               mpfr-print)
  #:export (prec))

(define prec (make-parameter 53))

(define-macro (private->public-2 f)
  (let ((private-f (string->symbol (string-append "_" (symbol->string f)))))
    `(define-public (,f x y)
      (,private-f x y (prec)))))

(define-macro (private->public-1 f)
  (let ((private-f (string->symbol (string-append "_" (symbol->string f)))))
    `(define-public (,f x)
      (,private-f x (prec)))))

(define-macro (private->public-0 f)
  (let ((private-f (string->symbol (string-append "_" (symbol->string f)))))
    `(define-public (,f)
      (,private-f (prec)))))

(private->public-1 integer->mpfr)
(private->public-2 mpfr-mul)
(private->public-2 mpfr-div)
(private->public-1 mpfr-sin)
(private->public-0 mpfr-pi)

Except for quasi-quoting and unquoting, which allows for a bit more flexibility (the computation with symbols part), this is essentially syntactic source code rewriting like for the C preprocessor. Said otherwise, this is very close to text file handling using a script in an editor, combinations of sed and awk on the command line, or with the Autotools.

Scheme purists, however, will worry about hygiene, although I think that the previous macros are a reasonable approach in our well circumscribed situation: They are not exported outside the module, in which they are used with very little context, so that referential transparency should not be an issue. If one wants to be in style, one should instead use define-syntax together with syntax-rules, or the define-syntax-rules shortcut. The previous approach of creating one symbol from another is not hygienically possible. So one needs to pass both the names of the existing hidden and the new public procedure as arguments, to obtain the following macro for a procedure with two arguments used to define multiplication:

(define-syntax-rule (private->public-2 private-f public-f)
  (define-public
    (public-f x y)
      (private-f x y (prec))))

(private->public-2 _mpfr-mul mpfr-mul)

Indeed the macro definition looks cleaner and less twisted, but there is a bit more boiler plate typing for using it. The resulting file mpfr.scm remains as compact, and the module can be used to run the same computations as previously:

(define-module (mpfr)
  #:use-module (mpfr-internal)
  #:re-export (mpfr->double
               mpfr-print)
  #:export (prec))

(define prec (make-parameter 53))

(define-syntax-rule (private->public-2 private-f public-f)
  (define-public
    (public-f x y)
      (private-f x y (prec))))

(define-syntax-rule (private->public-1 private-f public-f)
  (define-public
    (public-f x)
      (private-f x (prec))))

(define-syntax-rule (private->public-0 private-f public-f)
  (define-public
    (public-f)
      (private-f (prec))))

(private->public-1 _integer->mpfr integer->mpfr)
(private->public-2 _mpfr-mul mpfr-mul)
(private->public-2 _mpfr-div mpfr-div)
(private->public-1 _mpfr-sin mpfr-sin)
(private->public-0 _mpfr-pi mpfr-pi)

Think local

The parameterize construction of Guile brings us back to the functional idiom. Much like let, which creates local bindings, it takes a list of parameters and values to which the parameters are dynamically bound, so that these values are used in all computations internal to the envrionment. Since we already took care to export the prec parameter from the (mpfr) module, we can try it out immediately in the Guile REPL:

(use-modules (mpfr))
(parameterize ((prec 113))
  (mpfr-print (mpfr-mul (integer->mpfr 2) (mpfr-sin (mpfr-div (mpfr-pi) (integer->mpfr 3))))))
(mpfr-print (mpfr-mul (integer->mpfr 2) (mpfr-sin (mpfr-div (mpfr-pi) (integer->mpfr 3)))))

Notice how the first series of computations is carried out at local quadruple precision, whereas the value of prec remains globally unchanged, so that the second series is carried out with the default double precision.

To make things even more comfortable, we may hide the internal prec parameter and renounce at exporting it from the module (after all, its exact name does not matter), and instead export a bit of syntax:

(define-syntax-rule (with-prec p exp ...)
   (parameterize ((prec p))
     exp ...))

Then altogether, the previous example can be rewritten as

(use-modules (mpfr))
(define (sqrt3) (mpfr-mul (integer->mpfr 2) (mpfr-sin (mpfr-div (mpfr-pi) (integer->mpfr 3)))))
(with-prec 113
  (mpfr-print (sqrt3)))
(mpfr-print (sqrt3))

to compute an expression at two different precisions. (I have the vague impression that with-prec creates a monad; it is left as an exercise to the more knowledgeable reader to prove or disprove this statement.) The resulting code can be found in commit bae58c8.

Rounding modes

MPFR currently provides six rounding modes. When a result is exactly representable in the target precision, then is is returned as such. Otherwise the exact result falls in between two representable values, and it is rounded to one of them depending on the rounding mode. While in the MPFR C library the precision of a result is implicit and determined by the precision with which the variable holding it has been initialised, the rounding mode is chosen explicitly at each function invocation by passing one of six constants as an additional argument. Paradoxically, the easiest to understand rounding modes are the directed ones, which always round into the same direction, potentially depending on the sign of the number: MPFR_RNDU for rounding up (or towards +∞); MPFR_RNDD for rounding down (or towards -∞); MPFR_RNDZ for rounding towards 0 (or down for positive, up for negative values); MPFR_RNDA for rounding away towards infinity (or up for positive, down for negative numbers). Clearly these two come in pairs: If MPFR_RNDU returns one of the possibilities of a non-representable number, then MPFR_RNDD returns the other one, and the same holds for MPFR_RNDZ and MPFR_RNDA. The most conventional rounding mode, MPFR_RNDN, rounds to nearest, but suffers from an ambiguity when a number is exactly in the middle between two representable numbers, which are then both at the same distance. The strategy chosen by MPFR (and in fact the IEEE 754 standard) is to resolve ties to even, that is, to round towards the binary number with a 0 in the last position. Unlike its name suggests, MPFR_RNDF, or faithful rounding, is rather fickle: It non-deterministically returns one of the two possibilities, but without guaranteeing which one. So it is potentially faster at the price of losing reproducibility.

In guile-mpfr we export all these values to Guile as numerical constants. Rounding modes themselves can then be handled in an analogous way to the precision, by adding a hidden parameter (defaulting to MPFR_RNDN) and additional syntax with-rnd, which locally binds the rounding mode, and with-prec-rnd, which locally binds the precision and the rounding mode. Then the following code computes the square root of 3 at different precisions and rounding modes (it returns successively smaller values).

(use-modules (mpfr))
(define (sqrt3) (mpfr-mul (integer->mpfr 2) (mpfr-sin (mpfr-div (mpfr-pi) (integer->mpfr 3)))))
(mpfr-print (with-prec 113 (sqrt3)))
(mpfr-print (with-prec-rnd 113 MPFR_RNDD (sqrt3)))
(mpfr-print (with-rnd MPFR_RNDD (sqrt3)))

This finishes the mini-series on guile-mpfr; the current state of the code is given by commit b5cea0f. The scaffolding is in place, what is missing now is completing the functionality by wrapping all of MPFR!