ekmett / linear

Low-dimensional linear algebra primitives for Haskell.
http://hackage.haskell.org/package/linear
Other
201 stars 50 forks source link

Epsilon without Num #168

Open gilgamec opened 3 years ago

gilgamec commented 3 years ago

There doesn't seem to be anything in Epsilon that would require instances to also be instances of Num. I have some numeric newtypes that I'm making Additive but not full instances of Num that I'd like to still be Epsilon. Could the Num constraint be removed?

For instance, for dimensioned quantities:

newtype Angle = Radians Double
-- with appropriate instances, but not Num

birotate :: Angle -> Angle -> Point -> Point
birotate a1 a2
  | nearZero da = id
  | otherwise = mkRotation da
 where
  da = a1 + a2
RyanGlScott commented 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
    |                         ^^^^^^^^^
gilgamec commented 3 years ago

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
RyanGlScott commented 3 years ago

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?

gilgamec commented 3 years ago

How would a1 + a2 typecheck if Angle didn't have a Num 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 Functors, so you trade off one type of safety for another - but at least I can be explicit about messing with the encapsulated value.

RyanGlScott commented 3 years ago

Sorry, I apparently entered the function wrong. As I said, they're Additive, so it'd actually be

da = 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.

gilgamec commented 3 years ago

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 }?

RyanGlScott commented 3 years ago

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?