Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Epsilon without Num #168

Open
gilgamec opened this issue Oct 4, 2021 · 7 comments
Open

Epsilon without Num #168

gilgamec opened this issue Oct 4, 2021 · 7 comments

Comments

@gilgamec
Copy link

gilgamec commented Oct 4, 2021

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
Copy link
Collaborator

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
Copy link
Author

gilgamec commented Oct 5, 2021

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
Copy link
Collaborator

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
Copy link
Author

gilgamec commented Oct 5, 2021

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
Copy link
Collaborator

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
Copy link
Author

gilgamec commented Oct 5, 2021

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
Copy link
Collaborator

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants