Fixed-point Arithmetic in Picolisp
rick, 2016-08-27

For the impatient

Here's the lowdown.

  • Picolisp is a cool, very small language that exemplifies KISS.
  • Picolisp does not have floating point numbers.
  • You can use integers to do most of what you use floating point numbers for.

Before we start, if you're belly-aching or whining about not having floating point numbers to work with, don't worry. What we're about to do is not difficult at all or even terribly inconvenient. So, let's "put our big boy pants on" and get started.

First, decide how many digits to the right of the decimal point you want your program to keep track of. This is called the scale. Say, you want to keep track of 3 digits to the right of the decimal point. You just call the scl function like this. (By the way, scl sets a global called *Scl to the value of its argument.)

: (scl 3)
-> 3

Next, you enter numbers as normal, i.e. put a decimal point in the number. Let's try entering 19.46.

: 19.46
-> 19460

You can see here that picolisp interprets this as the integer 19460. The lower-order 3 digits are the fractional part. Simple.

Now, be careful of the automatic rounding.

: 0.2237683
-> 224

OK, here's how you add and subtract numbers.

: (+ 123.435 98134.349)
-> 98257784
: (- 123.435 98134.349)
-> -98010914

One more "gotcha": the representation of numbers with no fractional part should still contain a decimal point. Here's how not to add 42 to 78.231.

: (+ 42 78.231)
-> 78273

And here's how to do that correctly.

: (+ 42.0 78.231)
-> 120231

Multiplying and dividing is not as straight-forward as addition and subtraction. To do it, we rely on a very interesting primitive function in picolisp called */. Using this, the product of X and Y can be computed as (*/ X Y 1.0).

: (*/ 32.1 1.056 1.0)  # 32.1 * 1.056 = 33.8976
-> 33898

And the quotient of N and D can be computed as (*/ 1.0 N D).

: (*/ 1.0 91.8 1.323)  # 91.8 / 1.323 = 69.38775510...
-> 69388

Here's how you format output so that it is "human readable".

: (format (*/ 32.1 1.056 1.0) *Scl)  # again, 32.1 * 1.056 = 33.8976
-> "33.898"

Finally, you can control more rounding in the final formatting with the round function. Let's round to the tenths place.

: (round (*/ 32.1 1.056 1.0) 1)
-> "33.9"

Does some of this code look a bit wordy or busy to you? Well, it's Lisp; so, make some convenience functions. (Remember: big boy pants.) Now, go forth and create glorious hacks!

TL;DR1

Picolisp is a nice language that has simplicity at the core of its (or I should say, its author's) design philosophy. That's one of its aspects that makes it beautiful, in my view. And so, its simplicity with regard to the intrinsic data types leads us to an interesting issue: picolisp does not have floating point numbers, only integers. So, how does one, say, divide 1 by 2 to yield one-half?

As one sharp denizen of #picolisp on freenode put it, one could always "use fractions", which is to say, develop a fraction data-type/object and proceed with this. Even though he was joking when he said that, for some applications, the use of a fraction data-type can be very appropriate, if not, convenient. However, for most applications, most people feel more comfortable expressing those numbers with a decimal point; or, to be fair, the least we could say is that it seems that we all are very accustomed to such expressions. As evidence, notice how frequently we receive data from others that are in files that contain sequences of characters that look like "4.2" or "-0.42", and not such expressions as "3/4" or "3i + 4j".2

Fixed-point arithmetic

So what does one do with this kind of data when using a language that only has integers as a numeric type?

Well, they might "follow their nose" and use integers to represent numbers with fractional values. People in the wider world of computing had already devised ways of representing such values with integers and performing certain arithmetic operations on them. Their methods fall under the rubric of fixed-point arithmetic. To get a feel for this, let's start with a motivational example.

For instance, if I'm just adding dollars and cents on a calculator, I might type in the decimal point each time I enter an addend. I don't know about you, but I consider that a wasted motion — just don't type in the decimal point. This means that instead of entering dollars and cents, you are expressing the same value in cents only. So for a value like $4.25, enter 425. Then you keep track of the implicit decimal place yourself. So, if after adding a long list of prices in this way you end up with the number (integer) 132283, then what is your total? If you said, $1322.83, then give yourself a cigar!

Well, what did we just do? We used integers to represent numbers with fractional parts and performed arithmetic on them. That's the basic idea behind fixed-point arithmetic.

The same method works for subtraction (when one admits signed integers). Now, the challenge is how to handle multiplication and division. But let's get into that issue in the context of picolisp computations in the sections that follow.

But, before we proceed, note that the picolisp reference, when referring to fixed-point numbers, calls them "fixpoint numbers". Not to worry, as both terms refer to the same thing; I will continue to use the term "fixed-point" here.

The . reader macro

There are some facilities in picolisp which make it very convenient to write code that handles fixed-point arithmetic.

The first is the . reader macro. When the picolisp reader sees a number expressed with a decimal point — that is, the way we naturally write decimal numbers, e.g. 4.25 or 0.831 — it will convert it to an integer.

Having the reader convert such numbers into integers is not only a useful convenience, but as we will discuss later, yields at least one advantage in avoiding some hard-wiring of our code (hard-coding).

Now, "that's all well and good" that the reader will convert such expressions for you, but how does it know that, say, 1946 is meant to represent the fixed-point number 19.46 and not 1.946? Well, there is this idea of scale which is related to how many places to the right of the decimal point one wants to keep (depending on what is appropriate for the application of course). In picolisp, this is kept in a global called *Scl, and is usually set by the intrinsic function scl. An example is in order.

$ pil
: 1.946
-> 2
: (scl 3)
-> 3
: 1.946
-> 1946
: (scl 2)
-> 2
: 1.946
-> 195
: 19.46
-> 1946

Here's the play-by-play.

  • We start picolisp at the shell prompt $, and enter the REPL whose prompt is :.
  • When we first enter 1.946 at the REPL, the scale is set to 0 (by default), and picolisp reads the number as a fractional decimal and simply rounds to the nearest integer, and the REPL returns the value 2. By the way, the REPL prefixes its response value with ->.
  • We set the scale to 3, meaning that we would like picolisp to keep track of the fractional part up to 3 places.
  • Then, we see that the same input 1.946 is now represented by the integer 1946.
  • When we change the scale to 2,
  • this time 1.946 is represented by the integer 195; note the effect of rounding.
  • Finally, the input 19.46 is represented by the integer 1946.

So we can see where this idea of scale gets its name: in order to determine its integer representation, the decimal point number is conceptually "scaled up" by *Scl orders of magnitude (and then rounded to the nearest integer).

This is a good time to re-emphasize that it is only the reader — and not the evaluator — that is doing this conversion.3

: (scl 2)
-> 2
: (quote 19.46)
-> (1946)

Also, note that we can enter either 4.25 or 425 — the latter being similar to how we entered numbers in our calculator example — and picolisp will interpret this as the same number, namely 425.

Addition and subtraction

Now that we have the help of the reader for input of numbers with an embedded decimal point and the "scale facility" (i.e. scl and *Scl), adding and subtracting such numbers is fairly easy.

: (scl 3)
-> 3
: (+ 1092.339 933.29 -7.764382)
-> 2017865

The exact answer — given arbitrary precision — is 2017.864618. (Or, 1008932309/500000 if you have fractions. :) However, we had only asked to keep, and round to, the nearest thousandth, i.e., we said (scl 3).

Here's how to enter the same expression the way we did in the calculator example.

: (+ 1092339 933290 -7764)
-> 2017865

Notice that I manually rounded the last addend, simulating the rounding (to *Scl "fractional digits") that the picolisp reader would have done for us if we had instead entered -7.764382.

: -7.764382
-> -7764

If there is any "gotcha" to be concerned about, it is the one we mentioned in the first section: the representation of numbers with no fractional part (like a whole number) should be expressed with a zero fractional part. Recall this example from that section.

: (+ 42 78.231)
-> 78273

Oops, I had meant to add 42 to some number slightly greater than 78, so I thought the answer would be some number slightly greater than 120. How come this answer is still only slightly greater than 78? Hopefully, if you got caught in the "gotcha" as I did, at this point you remembered that the scale was, and is still now, set to 3; so, all numbers need to have three lower-order digits that represent the fractional part. So, 42 should be represented as 42000, or if you like 42.0 (and picolisp will convert it for you). And so, we finally conjure up the following, and once again, all is right with the world.

: (+ 42.0 78.231)
-> 120231

Multiplication and division

Multiplying and dividing these types of numbers is only slightly more involved than adding and subtracting.

To understand the method we are going to employ, let's try a simple example: let's multiply 0.6 by 7 giving (hopefully) 4.2, and let's use scale 4 (more than enough "room" on the fractional side).

: (scl 4)
-> 4
: (* 0.6 7.0)
-> 420000000

Oops, that's 42,000 (in "scale 4 land"). Do you see the problem? Since each factor in the product is scaled up by 104, our answer is off by 4 orders of magnitude. To compensate, we need to divide the product by 104.

There's a similar problem when we divide. Let's try dividing 4.2 by 0.6 to give 7.

: (/ 4.2 0.6)
-> 7

Ah, we got the right answer! … NO! Remember: we are in "scale 4 land" and so 7 is a representation of 0.0007, not 7; so actually we're four orders of magnitude off (again!). We need to compensate, this time by multiplying the quotient by 104.

Almost.

Look at this division example.

: (/ 4.2 7.0)
-> 0

Dang. We lost some critical information due to the integer divide. Now, if we "compensate" by multiplying the quotient by 104, as we said before, it does us no good: we'll still get zero.

To do this correctly, we will need to multiply the numerator by 104 first, then do the (integer) divide.

: (/ (* 4.2 10000) 7.0)
-> 6000

That's correct: 6000 is the integer representation of 0.6 in "scale 4". Here's a better way.

: (/ (* 4.2 1.0) 7.0)
-> 6000

In fact, using 1.0 in your code in this situation is much better than using 10000, which is scale-specific. When you use 1.0, you are letting picolisp do the scale conversion for you. Think about it: if you write an application using fixed-point arithmetic and you have "buyer's remorse" later and want to change the scale, you can simply change the scale once, and you don't have to change any other part of your source, if you used 1.0 for your conversion ("compensation") factor. If instead you peppered your code with the conversion factor of, say, 10000 (because you used scale 4), if you ever changed the scale, you'd have to chase down all the scale-specific factors and change them also.

Making things a bit easier

Finally, let's consider a primitive function in picolisp that will make multiplication and division a bit easier (and interestingly, which was borrowed from the programming language Forth4). It's called */ and it computes the product of all its arguments except for the last argument, and then divides this product by the last argument (and rounds, if necessary). Thus, (*/ 8 6 4) is 12.

So now, the product of fixed-point numbers X and Y can be computed by (*/ X Y 1.0) and the quotient of fixed-point numbers N and D can be computed by (*/ 1.0 N D).

Here is a reprise of the examples from the first section.5

: (*/ 32.1 1.056 1.0)  # 32.1 * 1.056 = 33.8976
-> 33898
: (*/ 1.0 91.8 1.323)  # 91.8 / 1.323 = 69.38775510...
-> 69388

Also note that the */ function rounds its final result. By the way, this is in contrast to the / function which simply truncates its final result.

: (*/ 11 3)
-> 4
: (/ 11 3)
-> 3

Finally, it should be noted that doing fixed-point arithmetic in picolisp with the */ operator is binary only, meaning that */ does not provide for variadic multiplication or division.

Formatting output

After your program is finished continually frobbing its fixed-point numbers, you will need it eventually to report results in the output in a "human readable" way — which is to say, in conventional terms with decimal points. Picolisp provides two functions that help you accomplish this.

The first is called format. In action:

: (scl 6)
-> 6
: (format 100123456 *Scl)
-> "100.123456"

The format function also does the inverse operation: converts string representations of numbers into integers. See the relevant reference entry for details.

For more control over the rounding in the final formatted output, see the round function.

: (round 100123456 4)
-> "100.1235"
: (round 100123456 2)
-> "100.12"

Considering only their respective output functionality, round can be viewed as a generalization of format.

: (round 100123456 *Scl)
-> "100.123456"

Check out the money function if you need accounting or financial output.

Final thoughts

The sqrt function in picolisp also supports fixed-point numbers.

: (scl 4)
-> 4
: (sqrt 64.0 1.0)
-> 80000

Having this function is convenient when we want to, say, compute the Euclidean distance in R2.6

One more caveat. We can't express fixed-point numbers with scientific notation, as we can with floating point numbers. For instance, 1e12 is not the integer 1 x 1012 or 1,000,000,000,000.7 The picolisp reader doesn't do anything special with, say, 1e12 as it does with, say, 1.12. So, the lesson is: just stick with expressing fixed-point numbers as numbers with embedded decimal points.

Fixed-point representations are more than just a language design trade-off that nods to simplicity, or even, ascetic austerity. I hope that this discussion has demonstrated enough examples to get a feel for how far one can go with these kind of very common computations for which we usually reach for floating point numbers, and when in fact they are not necessary. There is also a nice list of applications here which include games, graphics engines, and financial software, whose developers decided to forgo floating point calculations for fixed-point calculations.

A reason why these developers may have chosen to use fixed-point operations — that we did not discuss and that I wasn't going to originally address — is that of the exactness of fixed-point arithmetic as opposed to floating point arithmetic. I have included an appendix at the end of this article that illustrates the "exactness" issue with an example provided by picolisp's author Alexander Burger.

Happy hacking!

Appendix

Writes Alexander Burger:

Besides simplicity (and the slight inconveniences), scaled integer arithmetic has the advantage of absolute exactness for addition, subtraction and multiplication. This is a feature of integers in general, in contrast to floating point numbers.

Floats cannot represent most numbers exactly, but round them to 56 bits (in case of double precision numbers). This may cause errors to be accumulated. For example, this C program

int main(void) {
   double d = 0.1;
   int i;

   for (i = 0; i < 100000000; ++i)
      d += 0.1;
   printf("%9lf\n", d);
}

prints

10000000.081129

while a corresponding picolisp program

(scl 6)

(let D 0.1
   (do 100000000
      (inc 'D 0.1) )
   (prinl (format D *Scl)) )

(bye)

prints

10000000.100000

which is correct.

Footnotes:

1

Of course, the rest of this article is the "Too long; didn't read" portion. Fittingly, we have a title for this part, in the style of a 19th century book chapter title: On the arithmetik calculations of fractional numbers by computing machines solely using integer operations, in general; and in particular, how such is accomplished, in the picolisp language, by way of a discussion that so abuses the conversational style of writing and literary propriety as to drive its editor to imbibe in the extreme or to induce such physical pain onto himself in order to distract from the excruciating tortures of such a read.

2

Although, the world is certainly less interesting because more people don't use shilling fractions and complex numbers.

3

Since quote prevents its arguments from being evaluated, it must be the reader that is accomplishing this conversion.

4

This was conveyed to me by picolisp's author.

5

A bit of off-topic trivia: the exact answer for the second calculation below is 69 plus a fractional part that is a repeating decimal. This is the repeating portion: 387755102040816326530612244897959183673469. Yes, it's 42 digits long!

6

That's just "fancy talk" for finding the distance between two points in two-dimensional space.

7

In fact, in picolisp, 1e12 is a symbol.