Known/estabilished usecases for the monad instance of an homogeneous pair

Viewed 143

Once I asked this question, which was correctly marked as duplicate of this other one.

Now I have a curiosity, is there any known usecase for monad instance of a pair of homogeneous types?

Here are its instances:

data Pair a = Pair a a deriving Show

instance Functor Pair where
  fmap f (Pair a b) = Pair (f a) (f b)

instance Applicative Pair where
  pure a = Pair a a
  Pair f g <*> Pair x y =  Pair (f x) (g y)

instance Monad Pair where
    m >>= f = joinPair (f <$> m)

joinPair :: Pair (Pair a) -> Pair a
joinPair (Pair (Pair x _) (Pair _ y)) = Pair x y
2 Answers

Your Pair a is isomorphic to Reader Bool a/Bool -> a:

to (Pair f t) = \b -> if b then t else f

from f = Pair (f False) (f True)

As such, any use-case for the Reader monad is also a potential use-case for your monad. The general term for these sorts of data types is representable functors.

I have never used a monad like this before, but after coming up with this example, I can see its merits. This will calculate the distance between two cartesian coordinates. It seems to actually be quite useful because it automatically keeps any operations on Xs separate from any operations on Ys.

collapsePair :: (a -> a -> b) -> Pair a -> b
collapsePair f (Pair x y) = f x y

type Coordinates = Pair Float
type Distance = Float
type TriangleSides = Pair Distance

-- Calculate the sides of a triangle given two x/y coordinates
triangleSides :: Coordinates -> Coordinates -> TriangleSides
triangleSides start end = do
    -- Pair x1 y1
    s <- start

    -- Pair x2 y2
    e <- end

    -- Pair (x2 - x1) (y2 - y1)
    Pair (e - s) (e - s)
    
-- Calculate the cartesian distance
distance :: Coordinates -> Coordinates -> Distance
distance start end = collapsePair distanceFormula (triangleSides start end)
    where distanceFormula x y = sqrt (x ^ 2 + y ^ 2)

Edit:

It turns out this example could be done with just the Applicative instance of Pair; no Monad needed:

import Control.Applicative

triangleSides :: Coordinates -> Coordinates -> TriangleSides
triangleSides = liftA2 (flip (-))

But, we could make it depend on Monad in a contrived way, by adding 1 to X at the end:

triangleSides' :: Coordinates -> Coordinates -> TriangleSides
triangleSides' start end = do
    s <- start
    e <- end
    Pair (e - s + 1) (e - s)

In this case, the last line cannot be converted to some form of pure ... and therefore must use the Monad instance.


Something fun that can be explored further is that we can easily expand this to include three dimensional coordinates (or more). We can use the default instance of Representable and derive Applicative and Monad instances via the Co newtype from Data.Functor.Rep.

We can then abstract the distance calculation into its own typeclass and it will work on coordinates of n-dimensions, as long as we write a Distributive instance for the coordinate type.

{-# Language DeriveAnyClass #-}
{-# Language DeriveGeneric #-}
{-# Language DeriveTraversable #-}
{-# Language DerivingVia #-}

import Control.Applicative
import Data.Distributive
import Data.Functor.Rep
import GHC.Generics

class (Applicative c, Foldable c) => Coordinates c where
    distance :: Floating a => c a -> c a -> a
    distance start end = sqrt $ sum $ fmap (^2) $ liftA2 (-) end start

data Triple a = Triple
    { triple1 :: a
    , triple2 :: a
    , triple3 :: a
    }
    deriving ( Show, Eq, Ord, Functor, Foldable, Generic1, Representable
             , Coordinates )
    deriving (Applicative, Monad) via Co Triple

instance Distributive Triple where
    distribute f = Triple (triple1 <$> f) (triple2 <$> f) (triple3 <$> f)
> distance (Triple 7 4 3) (Triple 17 6 2)
10.246950765959598

You can verify this answer here.

Related