4

I'm trying to implement a bigdecimal in Idris. I have this so far:

-- a big decimal has a numerator and a 10^x value
-- it has one type for zero, 
--TODO the numerator can't be zero for any other case
--TODO and it can't be divisible by 10
data BigDecimal : (num : Integer) -> (denExp : Integer) -> Type where
  Zero : BigDecimal 0 0
  BD : (n : Integer) -> (d : Integer) -> BigDecimal n d 

I would like to force the restrictions marked by "TODO" above. However, I'm am just learning Idris, so I'm not even sure if this sort of restriction is a good idea or not.

In general I'm trying to create a tax calculation tool capable of computing with multiple (crypto)currencies using arbitrary precision. I'd like to be able to then try and use the prover to prove some properties about the program.

So, my questions are:

  • Is it a good design decision to try to enforce the restrictions I have specified?
  • Is it possible to do this kind of restriction in Idris?
  • Is this a good implementation of a BigDecimal in Idris?

Edit: I was thinking of something like "BD : (n : Integer) -> ((n = 0)=Void) -> (d : Integer) -> BigDecimal n d", so you have to pass a proof that n isn't zero. But I really don't know if this is a good idea or not.

Edit 2: In response to Cactus's comment, would this be better?

data BigDecimal : Type where
    Zero : BigDecimal
    BD : (n : Integer) -> (s : Integer) -> BigDecimal
redfish64
  • 565
  • 3
  • 10
  • 1
    Generally, BigDecimals consist of a BigInteger for the digits (for the unscaled value) and a scale or exponent indicating where the decimal point is supposed to be (this can be a negative value too, so you can represent very large values too). Unfortunately I don't know Idris, so I can't tell you how to do that in that language. Your idea with a numerator sounds more like a BigFraction or BigRational. That would complicate matters a little, IMO. – Rudy Velthuis Nov 12 '16 at 18:12
  • 1
    Actually I am using the same idea you are talking about. I probably should not have called it numerator, but I was thinking (numerator) / (10 ** (denominator exponent)). – redfish64 Nov 12 '16 at 22:56
  • Then the naming is indeed weird. Your numerator is the unscaled value I meant and your denominator exponent is the scale. So the value 1.000 is represented as unscaled value 1000 and scale 3. Just be sure to scale up or down properly when you add or subtract, and to adjust the scale when you multiply or divide. Also, if you divide, scale the numerator up by the precision you want and round the result properly. I.e. with a precision of 10, multiply the numerator by 10**10 and then divide, otherwise 1 / 10 could result in 0 instead of 0.1000000000. – Rudy Velthuis Nov 12 '16 at 23:32
  • Your current design means every number is represented by a separate, singleton type -- I don't think that's what you want. Why do you have the numerator and the magnitude as indices? – Cactus Nov 13 '16 at 03:03
  • @"Rudy Velthuis" The point is to make it so 1000 isn't a valid numerator. Thats what "TODO and it can't be divisible by 10" is supposed to restrict, if I can figure it out. – redfish64 Nov 13 '16 at 07:31

1 Answers1

3

You could just spell out your invariants in the constructor types:

data BigDecimal: Type where
     BDZ: BigDecimal
     BD: (n : Integer) -> {auto prf: Not (n `mod` 10 = 0)} -> (mag: Integer) -> BigDecimal

Here, prf will ensure that n is not divisible by 10 (which also means it will not be equal to 0), thereby ensuring canonicity:

  • The only representation of 0 is BDZ
  • The only representation of n * 10mag is BD n mag: BD (n * 10) (mag - 1) is rejected because n * 10 is divisible by 10, and since n itself is not divisible by 10, BD (n / 10) (mag + 1) would not work either.

EDIT: It turns out, because Integer division is non-total, Idris doesn't reduce the n `mod` 10 in the type of the constructor BD, so even something as simple as e.g. BD 1 3 doesn't work.

Here's a new version that uses Natural numbers, and Data.Nat.DivMod, to do total divisibility testing:

-- Local Variables:
-- idris-packages: ("contrib")
-- End:

import Data.Nat.DivMod
import Data.So

%default total

hasRemainder : DivMod n q -> Bool
hasRemainder (MkDivMod quotient remainder remainderSmall) = remainder /= 0

NotDivides : (q : Nat) -> {auto prf: So (q /= 0)} -> Nat -> Type
NotDivides Z {prf = Oh} n impossible
NotDivides (S q) n = So (hasRemainder (divMod n q))

Using this, we can use a Nat-based representation of BigDecimal:

data Sign = Positive | Negative

data BigNatimal: Type where
     BNZ: BigNatimal
     BN: Sign -> (n : Nat) -> {auto prf: 10 `NotDivides` n} -> (mag: Integer) -> BigNatimal

which is easy to work with when constructing BigNatimal values; e.g. here's 1000:

bn : BigNatimal
bn = BN Positive 1 3

EDIT 2: Here's a try at converting Nats into BigNatimals. It works, but Idris doesn't see fromNat' as total.

tryDivide : (q : Nat) -> {auto prf : So (q /= 0)} -> (n : Nat) -> Either (q `NotDivides` n) (DPair _ (\n' => n' * q = n))
tryDivide Z {prf = Oh} n impossible
tryDivide (S q) n with (divMod n q)
  tryDivide _ (quot * (S q)) | MkDivMod quot Z _ = Right (quot ** Refl)
  tryDivide _ (S rem + quot * (S q)) | MkDivMod quot (S rem) _ = Left Oh

fromNat' : (n : Nat) -> {auto prf: So (n /= 0)} -> DPair BigNatimal NonZero
fromNat' Z {prf = Oh} impossible
fromNat' (S n) {prf = Oh} with (tryDivide 10 (S n))
  fromNat' (S n) | Left prf = (BN Positive (S n) {prf = prf} 1 ** ())
  fromNat' _ | Right (Z ** Refl) impossible
  fromNat' _ | Right ((S n') ** Refl) with (fromNat' (S n'))
    fromNat' _ | Right _ | (BNZ ** nonZero) = absurd nonZero
    fromNat' _ | Right _ | ((BN sign k {prf} mag) ** _) = (BN sign k {prf = prf} (mag + 1) ** ())

fromNat : Nat -> BigNatimal
fromNat Z = BNZ
fromNat (S n) = fst (fromNat' (S n))
Cactus
  • 27,075
  • 9
  • 69
  • 149