A
LogFloat is just a
Double with a special
interpretation. The
logFloat function is presented instead of
the constructor, in order to ensure semantic conversion. At present
the
Show instance will convert back to the normal-domain, and
hence will underflow at that point. This behavior may change in the
future. At present, the
Read instance parses things in the
normal-domain and then converts them to the log-domain. Again, this
behavior may change in the future.
Because
logFloat performs the semantic conversion, we can use
operators which say what we
mean rather than saying what we're
actually doing to the underlying representation. That is, equivalences
like the following are true[1] thanks to type-class overloading:
logFloat (p + q) == logFloat p + logFloat q
logFloat (p * q) == logFloat p * logFloat q
(Do note, however, that subtraction can and negation will throw
errors: since
LogFloat can only represent the positive half
of
Double.
Num is the wrong abstraction to put at the
bottom of the numeric type-class hierarchy; but alas, we're stuck with
it.)
Performing operations in the log-domain is cheap, prevents underflow,
and is otherwise very nice for dealing with miniscule probabilities.
However, crossing into and out of the log-domain is expensive and
should be avoided as much as possible. In particular, if you're doing
a series of multiplications as in
lp * logFloat q * logFloat
r it's faster to do
lp * logFloat (q * r) if you're
reasonably sure the normal-domain multiplication won't underflow;
because that way you enter the log-domain only once, instead of twice.
Also note that, for precision, if you're doing more than a few
multiplications in the log-domain, you should use
product
rather than using
(*) repeatedly.
Even more particularly, you should
avoid addition whenever
possible. Addition is provided because sometimes we need it, and the
proper implementation is not immediately apparent. However, between
two
LogFloats addition requires crossing the exp/log boundary
twice; with a
LogFloat and a
Double it's three times,
since the regular number needs to enter the log-domain first. This
makes addition incredibly slow. Again, if you can parenthesize to do
normal-domain operations first, do it!
- 1 That is, true up-to underflow and floating point
fuzziness. Which is, of course, the whole point of this module.