There are two problems here. Double
arithmetic is defined with primitive functions. Idris can't even proof that a <= b = True -> b <= c = True -> a <= c = True
(which, by the way, does not even hold all the time - so this is not Idris' fault.) There is no proof for a <= b = True
other then just checking it, what you tried with the ifThenElse
.
When working with such blind run-time proofs (so just … = True
), Data.So
is quite helpful. ifThenElse (a <= x) … …
branches off given a boolean check, but the code in the branches does not know about result of the check. With choose (a <= x)
you get the result for the branches, with Left prf
and prf : So (a <= x)
or Right prf
and prf : So (not (a <= x))
.
I suppose if the result of adding two bounded doubles would be bigger then the upper bound, the result should be this upper bound. Lets make a first attempt:
import Data.So
data BoundedDouble : (a, b : Double) -> Type where
MkBoundedDouble : (x : Double)
-> {auto high : So (a <= x)}
-> {auto low : So (x <= b)}
-> BoundedDouble a b
Num (BoundedDouble a b) where
(+) (MkBoundedDouble u) (MkBoundedDouble v) =
let x = u + v
in case (choose (a <= x), choose (x <= b)) of
(Left _, Left _) => MkBoundedDouble x
(Right _, _) => ?holeMin
(_, Right _) => ?holeMax
This already typechecks, but has holes in it. We want to set ?holeMin
to MkBoundedDouble a
and ?holeMax
to MkBoundedDouble b
. However, MkBoundedDouble
right now needs two proofs: high
and low
. In the case of ?holeMax
those would be with x = b
So (a <= b)
and So (b <= b)
. Again, Idris does not know that b <= b
for every b : Double
. So we would need to choose again to get these proofs:
(_, Right _) => case (choose (a <= b), choose (b <= b)) of
(Left _, Left _) => MkBoundedDouble b
_ => ?what
Because Idris cannot see that b <= b
, the function would be partial. We could cheat and use for example MkBoundedDouble u
in ?what
, so the function will typecheck and hope that this will indeed never occur.
There is also the possibility to convince the type checker with force that b <= b
is always true:
data BoundedDouble : (a, b : Double) -> Type where
MkBoundedDouble : (x : Double)
-> {auto rightSize : So (a <= b)}
-> {auto high : So (a <= x)}
-> {auto low : So (x <= b)}
-> BoundedDouble a b
DoubleEqIsSym : (x : Double) -> So (x <= x)
DoubleEqIsSym x = believe_me (Oh)
Num (BoundedDouble a b) where
(+) (MkBoundedDouble u) (MkBoundedDouble v) =
let x = u + v
in case (choose (a <= x), choose (x <= b)) of
(Left _, Left _) => MkBoundedDouble x
(Right _, _) => MkBoundedDouble a {high=DoubleEqIsSym a}
(_, Right _) => MkBoundedDouble b {low=DoubleEqIsSym b}
Or we could be even safer and put the proofs for the upper and lower bounds in the data constructor, so we can use them in ?holeMin
and ?holeMax
. This would be:
import Data.So
data BoundedDouble : (a, b : Double) -> Type where
MkBoundedDouble : (x : Double)
-> {auto rightSize : So (a <= b)}
-> {auto leftId : So (a <= a)}
-> {auto rightId : So (b <= b)}
-> {auto high : So (a <= x)}
-> {auto low : So (x <= b)}
-> BoundedDouble a b
Num (BoundedDouble a b) where
(+) (MkBoundedDouble u) (MkBoundedDouble v) =
let x = u + v
in case (choose (a <= x), choose (x <= b)) of
(Left _, Left _) => MkBoundedDouble x
(Right _, _) => MkBoundedDouble a
(_, Right _) => MkBoundedDouble b
You see that even that the constructor is packed with proofs, they don't complicate the implementation. And they should get erased in the actual run-time code.
However, as an exercise you could try to implement Num
for
data BoundedDouble : (a, b : Double) -> Type where
MkBoundedDouble : (x : Double)
-> {auto rightSize : So (a <= b)}
-> {auto high : So (a <= x)}
-> {auto low : So (x <= b)}
-> BoundedDouble a b
Min : {auto rightSize : So (a <= b)} -> BoundedDouble a b
Max : {auto rightSize : So (a <= b)} -> BoundedDouble a b
Sadly, there aren't many resources for Idris yet. Besides the tutorial there is a book in development, that I would recommend. It gives more approachable exercises than working with primitive types. :-)