2
\$\begingroup\$

I wanted to create a type or class, which can be used to handle angles. I started with defining a class, to make it possible to store degrees in Integral, radians in Floating, etc... But I realized, that I will meet complex problems if I continue it so. For example handling the addition of two angles, like a Radian Double and a Degree Int.

To keep it simple, easy to use and develop, I decided to create one type only, and write the functions around it.

module Data.Angle where

import Data.Fixed -- mod'

data Angle a = Radians a deriving (Eq, Show)


-- Creating Angle from a value


-- | Create an Angle with the given degrees
angleFromDegrees :: (Integral d, Floating r) => d -> Angle r
angleFromDegrees x = Radians $ (realToFrac x) * pi/180

-- | Create an Angle with the given turns
angleFromTurns :: (Real t, Floating r) => t -> Angle r
angleFromTurns x = Radians $ (realToFrac x) * pi*2

-- | Create an Angle with the given turns
angleFromRadians :: (Floating r) => r -> Angle r
angleFromRadians x = Radians x


-- Get the value from Angle


-- | Get degrees from an Angle
angleValueDegrees :: (Floating r, RealFrac r, Integral d) => Angle r -> d
angleValueDegrees (Radians x) = round $ x / pi * 180.0

-- | Get radians from an Angle
angleValueRadians :: (Floating r) => Angle r -> r
angleValueRadians (Radians x) = x

-- | Get turns from Angle
angleValueTurns :: (Floating r) => Angle r -> r
angleValueTurns (Radians x) = x / (pi*2)


-- Basic functions


-- | Adding two angles
addAngle :: (Floating a) => Angle a -> Angle a -> Angle a
addAngle (Radians r1) (Radians r2) = Radians $ r1 + r2

-- | Normalize Angle: transforming back to (0-2pi)
normAngle :: (Floating a, Real a) => Angle a -> Angle a
normAngle (Radians r) = Radians $ mod' r (pi*2)

-- | Add two angles and normalize the result
addAngleNorm :: (Floating a, Real a) => Angle a -> Angle a -> Angle a
addAngleNorm a b = normAngle $ addAngle a b

-- | Distance between two angles
distAngle :: (Floating a, Real a) => Angle a -> Angle a -> Angle a
distAngle (Radians r1) (Radians r2) = Radians $ if (a' < b') then a' else b'
    where
        a' = mod' (r1-r2) (pi*2)
        b' = mod' (r2-r1) (pi*2)

-- | Flip Angle
flipAngle :: (Floating a) => Angle a -> Angle a
flipAngle (Radians r) = Radians (-r)

-- | Flip Angle and normalize the result
flipAngleNorm :: (Floating a, Real a) => Angle a -> Angle a
flipAngleNorm = normAngle . flipAngle

-- | Add degrees to Angle
addAngleDegrees :: (Floating r, Integral d) => Angle r -> d -> Angle r
addAngleDegrees ang deg = addAngle ang $ angleFromDegrees deg

-- | Add radians to Angle
addAngleRadians :: (Floating r) => Angle r -> r -> Angle r
addAngleRadians (Radians r1) r2 = Radians $ r1 + r2

-- | Add turns to Angle
addAngleTurns :: (Floating r, Real t) => Angle r -> t -> Angle r
addAngleTurns ang turn = addAngle ang $ angleFromTurns turn

-- Trigonometric functions


-- | Sine of the angle
sinAngle :: (Floating a) => Angle a -> a
sinAngle (Radians r) = sin r

-- | Cosine of the angle
cosAngle :: (Floating a) => Angle a -> a
cosAngle (Radians r) = cos r

-- | Tangent of the angle
tanAngle :: (Floating a) => Angle a -> a
tanAngle (Radians r) = tan r

-- | Cotangent of the angle
cotAngle :: (Floating a) => Angle a -> a
cotAngle (Radians r) = 1 / (tan r)


-- Inverse trigonometric functions


-- | Create angle from inverse sine
asinAngle :: (Floating a) => a -> Angle a
asinAngle x = Radians $ asin x

-- | Create angle from inverse cosine
acosAngle :: (Floating a) => a -> Angle a
acosAngle x = Radians $ acos x

-- | Create angle from inverse tangent
atanAngle :: (Floating a) => a -> Angle a
atanAngle x = Radians $ atan x

-- | Create angle from inverse cotangent
acotAngle :: (Floating a) => a -> Angle a
acotAngle x = Radians $ (pi/2) - (atan x)

The finished library is available here:

Haskell Angle Library

\$\endgroup\$
1
\$\begingroup\$

I would use newtype instead of data to define Angle, like this:

newtype Angle a = Radians { angleValueRadians :: a }

This will automatically create the angleValueRadians function to get the angle value, and will be a bit more efficient.

Here is a good answer explaining the differences between data, newtype, and type: https://stackoverflow.com/a/21081227/1525759

You could use function composition to define many of your functions instead of pattern matching (pattern matching isn't bad, I just prefer this style):

sinAngle :: (Floating a) => Angle a -> a
sinAngle = sin . angleValueRadians

Also, if you made Angle an instance of Applicative, you could define addAngle like this:

addAngle :: (Floating a) => Angle a -> Angle a -> Angle a
addAngle r1 r2 = (+) <$> r1 <*> r2
-- or even this, because `f <$> x <*> y` is the same as `liftA2 f x y`:
addAngle = liftA2 (+)

Here's how you could do that:

instance Functor Angle where
    fmap f (Radians x) = Radians (f x)

instance Applicative Angle where
    pure = Radians       
    Radians f <*> r = fmap f r
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.