3

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
Enlico
  • 23,259
  • 6
  • 48
  • 102

2 Answers2

5

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.

  • 1
    The `Monad` instance of `Pair` is different from `Reader`, however. – danidiaz Mar 11 '21 at 18:52
  • 1
    @danidiaz It is? It looks equivalent to me. – Joseph Sible-Reinstate Monica Mar 11 '21 at 18:54
  • @JosephSible-ReinstateMonica @danidiaz In a way you're both right, because any `Representable` can [automatically be an instance of MonadReader](https://hackage.haskell.org/package/adjunctions-4.4/docs/Data-Functor-Rep.html#v:askRep). In this case, `ask` would return `Pair False True`. – hololeap Mar 11 '21 at 19:08
  • @danidiaz, if `Pair a` and `Reader Bool a` are isomorphic doesn't it mean that in whatever way one is a monad, the other one is a monad in the same way too? – Enlico Mar 11 '21 at 20:20
  • 1
    @Enlico Yes. Does that sound like it contradicts something somebody else said to you? (It doesn't sound that way to me.) – Daniel Wagner Mar 11 '21 at 22:22
  • 1
    @danidiaz, looks the same to me too. – dfeuer Mar 12 '21 at 00:23
  • @DanielWagner, danidiaz wrote that `Pair` and `Reader` (I guess he meant `Reader Bool`...) have different `Monad` instances, _however_, implying that this answer is correct in saying that `Pair a` and `Reader Bool a` are isomorphic. If they are isomorphic, and [`Pair` can be made `Monad` in only one way](https://stackoverflow.com/a/58754645/5825294), shouldn't `Reader Bool` be a `Monad` in the same only way? – Enlico Mar 12 '21 at 08:08
  • @Enlico I took the answer to mean that `join` and `>>=` for `Pair` are equivalent to `join` and `>>=` for `Reader`, which they don't seem to be. It does seem that, because `Pair` is both `Monad` and `Representable`, it can be given a `MonadReader` instance "mechanically". But can we derive the `join` and `>>=` "mechanically" as well, alone from the fact that `Pair` is `Representable` ? – danidiaz Mar 12 '21 at 08:50
  • @danidiaz, ok, in this case I guess I need to study a bit more to really understand your point. Thanks for this food for thought. – Enlico Mar 12 '21 at 08:55
  • 2
    @danidiaz Again, why do you say they don't seem to be equivalent? Can you provide a concrete case of when they behave differently? – Joseph Sible-Reinstate Monica Mar 12 '21 at 14:43
  • 1
    @Enlico Yes, `Pair` can be made `Monad` in only one way, and that way is exactly the one given here, and also exactly the one you get by pushing the `Reader Bool` instance through the isomorphism. danidiaz is confused when they claim that the instance in this answer and the `Reader Bool` instance aren't connected by the isomorphism. – Daniel Wagner Mar 12 '21 at 16:13
  • 1
    @JosephSible-ReinstateMonica Indeed I was wrong, I see it now. I have one lingering question: `Pair a a` is isomorphic to `Reader Bool a`, but unlike `Reader Bool a` it's not a `MonadReader Bool`. Or is it, as well? – danidiaz Mar 12 '21 at 18:27
  • 1
    @JosephSible-ReinstateMonica Ah, forget it, of course `Pair a a` is a `MonadReader Bool`. I guess my brain doesn't play well with `Pair a a`! – danidiaz Mar 12 '21 at 18:39
  • 1
    @danidiaz It is. The easiest way to see this is to just use the isomorphism: `instance MonadReader Bool Pair where { ask = from ask; local f = from . local f . to }` – Joseph Sible-Reinstate Monica Mar 12 '21 at 18:39
4

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.

hololeap
  • 1,156
  • 13
  • 20
  • The distance formula comes from the [Pythagorean theorem](https://en.wikipedia.org/wiki/Pythagorean_theorem), where the hypotenuse of a triangle, `c`, is found given its sides `a` and `b`: `a^2 + b^2 = c^2`. – hololeap Mar 11 '21 at 18:28
  • 2
    Oh, ok. Thanks. However, I don't think you need the Monad instance for that. What does it do more than the Applicative `(-) <$> end <*> start` does? Actually you could do `(.-.) = liftA2 (-)` and then go for `end .-. start`, which is far more readable. (By the way, +1 for the inventive!) – Enlico Mar 11 '21 at 18:31
  • That's a good point, and this example could be done with Applicative instead of Monad. (The last line of the `do` block could be `pure (e - s)`.) I suppose there could be intermediary calculations that would require Monad for some other (better) example. – hololeap Mar 11 '21 at 18:55
  • 1
    While we can't derive `Distributive` for now there is a default definition for all `Representable` functors: `distribute = distributeRep`. – Iceland_jack May 09 '21 at 16:53
  • On spelling it with `Applicative`: `(>>=)` and `(<*>)` [are equivalent for functions](https://stackoverflow.com/a/40136614) and other `Representable` functors. That being so, in cases like this it is always possible to convert to `Applicative`. `triangleSides'`, for instance, might be defined as `Pair (\s e -> s - e + 1) (\s e -> s - e) <*> start <*> end`. (In any case, that doesn't invalidate your examples -- I do like how do-notation looks in `triangleSides`!) – duplode Feb 09 '22 at 02:26