Scheme's numbers are not like the rest of its library. They're older, and they're mostly borrowed from other languages (Maclisp and Common Lisp), so they follow those languages' style rather than Scheme's. They're designed more for the convenience of users than of theorists; they have a usefully complete feature set; they have a printed representation; their operations are predefined and polymorphic and have very short names.
What would Scheme be like if numbers followed the same style as the rest of the language?
It would be necessary to import a library before using any numbers.
(import (scheme numbers))
Numeric constants would be provided as functions returning the constant, apparently because the section of RNRS they appear in is called “Standard Procedures”. Only the most basic constants would be provided; pi
would not be among them.
(define (exact-rational-zero)
(make-exact-rational (exact-integer-zero) (exact-integer-one)))
Numbers would have no printed representation. Creating them would require explicit constructor calls.
There would be no polymorphism. Most operations would include a type in their name.
(define (factorial n)
(if (exact-integer<=? n (exact-integer-one))
(exact-integer-one)
(exact-integer-multiply! (factorial (exact-integer-subtract n (exact-integer-one))) n)))
The distinction between exact and inexact numbers would still be supposedly “orthogonal to the dimension of type”. But the lack of polymorphism would make it even more obvious that in practice exactness was simply one of the type distinctions: that between floats and everything else.
Floating-point numbers would be called “inexact rationals”. Their constructor would take a numerator and denominator, just like exact rationals; their floating-point representation would be considered an implementation detail. Various details of the specification would be inconsistent with IEEE floating point.
NaN
would not be a number, of course. inf.0
and -inf.0
would be exact transfinite numbers, not inexact rationals. There would be no negative zero.
Names would be descriptive, like inexact-rational-square-root
and exact-integer-greatest-common-divisor
.
There would be exact-integer->list
and list->exact-integer
operations to convert to and from lists of digits (in arbitrary bases). Converting the lists into strings would be up to you. Converting anything other than exact integers to strings would also be up to you.
Numbers would be portably mutable. Some operations would have destructive versions. (If we did this exercise on Python, some would have only destructive versions.) Racket would omit these, supposedly to make optimization easier, but would have separate mutable numbers for programs that need them.
Operations more obscure than exponent
would be left to SRFIs. Users would be able to choose between the widely supported SRFI and the complete SRFI.
exact-integer-divide
would not be provided, on the grounds that it's not defined for all integers, and can't be implemented efficiently without special hardware.
There would be a portable way to use exact integers as indexes into lists, but not into vectors or strings. This would be remedied in R7RS.
Some implementations would support surprisingly obscure and practical floating-point operations, while omitting basic operations their authors never needed.
(define (numerically-stable? thunk tolerance)
"Run a floating-point computation with various rounding modes to see
if this significantly changes the result. This is not a reliable test
of numeric stability, but it's an easy way to find bugs."
(let ((down (call-with-rounding-mode round-down thunk))
(up (call-with-rounding-mode round-up thunk))
(nearest (call-with-rounding-mode round-to-nearest thunk))
(zero (call-with-rounding-mode round-to-zero thunk))
(roughly-equal? (lambda (a b)
(inexact-rational<=?
(inexact-rational-absolute-value
(inexact-rational-subtract a b))
tolerance)))))
(and (roughly-equal? down up)
(roughly-equal? down nearest)
(roughly-equal? down zero)
(roughly-equal? up nearest)
(roughly-equal? up zero)
(roughly-equal? nearest zero)))
There would be debates about whether eq?
should “work” on numbers. This would really be about whether numeric operations should always return fresh numbers, and whether the compiler would be allowed to copy them, but no one would mention these merely implementational issues.
eqv?
and equal?
would compare numbers, even immutable ones, by identity. Hashtables would — OK, standard Scheme doesn't have hashtables. But if it did, the default hash function would hash numbers by identity, not by value.
Arithmetic overflow would still be “a violation of an implementation restriction”. There would still be no way to find out how large a number could safely be.
There would still be no bitwise operations on integers. Schemers who understood the purpose would advise using an implementation that supports bitvectors instead of abusing numbers. Those who did not would say they're easy to implement.
(define two (exact-integer-add (exact-integer-one) (exact-integer-one)))
(define (exact-integer-bitwise-and a b)
(list->exact-integer (map exact-integer-minimum
(exact-integer->list a two)
(exact-integer->list b two))))
Complex numbers would, mercifully, be left to a SRFI. The SRFI number would be real, but in most implementations complex-number support would be purely imaginary.
All the comparison predicates would end in ?
.
Edit: Replaced some stray uses of <=
and +
and min
with their counterfactual-Scheme equivalents.
In the HN comments, cousin_it says:
We can see similar examples in other languages, e.g. C++ strings are "like C++" and a pain to use, while Java strings are "not like Java" and a pleasure to use. Maybe language design really isn't about general-purpose elegance, but about finding good special-purpose solutions.
Or about using the good general-purpose solutions you already have.
You might be amused by reading:
ReplyDeleteSebastian Egner, Richard Kelsey, Michael Sperber: Cleaning up the Tower: Numbers in Scheme, In The 2004 Scheme Workshop, Snowbird, Utah, October 2004.
http://www.deinprogramm.de/sperber/papers/numerical-tower.pdf
I've been thinking for the last few years that the Right Thing is to adapt the OCaml idea of two kinds of operators to dynamic typing in which there is only one kind of numeric value. In this Scheme of things, 0.1 and 1/10 are the same value, but whereas (+ 1/10 1/10) => 2/10, (+. 1/10 1/10) => 0.200000000000000011102230246252 or 3602879701896397/18014398509481984.
ReplyDelete