Open gilgamec opened 3 years ago
The Num
superclass is used to the benefit in the Epsilon
instances for fixed-length vector types. For instance, removing the Num
superclass would make the Epsilon (V n a)
instance fail to compile:
[ 7 of 22] Compiling Linear.V ( src/Linear/V.hs, interpreted )
src/Linear/V.hs:422:25: error:
• Could not deduce (Num a) arising from a use of ‘quadrance’
from the context: (Dim n, Epsilon a)
bound by the instance declaration at src/Linear/V.hs:421:10-46
Possible fix:
add (Num a) to the context of the instance declaration
• In the second argument of ‘(.)’, namely ‘quadrance’
In the expression: nearZero . quadrance
In an equation for ‘nearZero’: nearZero = nearZero . quadrance
|
422 | nearZero = nearZero . quadrance
| ^^^^^^^^^
Clearly that makes sense, as you have to have some way to combine the different dimensions (though you could also use an infinity-norm, i.e. nearZero = all nearZero
). But couldn't you just move Num a
into the constraint of the instance where it's needed?
instance (Dim n, Epsilon a, Num a) => Epsilon (V n a) where
nearZero = nearZero . quadrance
I suppose. But it's difficult to imagine a situation where you'd need Epsilon
without Num
(or a subclass of Num
). Even in your original example, you have:
birotate :: Angle -> Angle -> Point -> Point
birotate a1 a2
| nearZero da = id
| otherwise = mkRotation da
where
da = a1 + a2
How would a1 + a2
typecheck if Angle
didn't have a Num
instance?
How would
a1 + a2
typecheck ifAngle
didn't have aNum
instance?
Sorry, I apparently entered the function wrong. As I said, they're Additive
, so it'd actually be
da = a1 ^+^ a2
I do this for types which have useful additive properties (and maybe even subtraction and negation) but for which multiplying them doesn't make sense; you can still do scalar multiplication with *^
, but an angle times an angle, or a kilogram times a kilogram, aren't something I want to deal with. A quantity with unit is essentially a vector space, then, right?
Of course, they have to be Functor
s, so you trade off one type of safety for another - but at least I can be explicit about messing with the encapsulated value.
Sorry, I apparently entered the function wrong. As I said, they're
Additive
, so it'd actually beda = a1 ^+^ a2
Ah, OK. Does that mean that Angle
has a different kind than what you posed above? I ask since if the definition is newtype Angle = Radians Double
, then Additive Angle
doesn't kind-check.
That'll show me for summarizing a piece of code for a bug report! Here's the entire definition:
newtype Angle a = Angle{ inRadians :: a }
deriving (Eq,Ord,Show,Functor)
instance Applicative Angle where
pure = Angle
(Angle f) <*> (Angle x) = Angle (f x)
instance Additive Angle where
zero = Angle 0
radians :: a -> Angle a
radians = Angle
inDegrees :: Floating a => Angle a -> a
inDegrees (Angle r) = r * (180 / pi)
degrees :: Floating a => a -> Angle a
degrees a = Angle (a * pi / 180)
I can in this way refer consistently to angles without having to worry if I'm getting degrees or radians. I could of course just implement Num
from Applicative
; then, for instance, I could double an angle with 2 * theta
, which would be equivalent to Angle 2 * theta
. But then what happens if I change the definition of Angle
to Angle{ inDegrees :: a }
?
Thanks, that explanation helps. In that case, I think I'm on board with the idea of removing the Num
superclass. Can you think of any complications that would arise from this, @ekmett?
There doesn't seem to be anything in
Epsilon
that would require instances to also be instances ofNum
. I have some numericnewtype
s that I'm makingAdditive
but not full instances ofNum
that I'd like to still beEpsilon
. Could theNum
constraint be removed?For instance, for dimensioned quantities: