{-# LANGUAGE DeriveFunctor      #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE RankNTypes         #-}
{-# LANGUAGE TypeOperators      #-}

-- |
-- Module      : Control.Natural.IsoF
-- Copyright   : (c) Justin Le 2019
-- License     : BSD3
--
-- Maintainer  : justin@jle.im
-- Stability   : experimental
-- Portability : non-portable
--
-- Types describing isomorphisms between two functors, and functions to
-- manipulate them.
module Control.Natural.IsoF (
    type (~>)
  , type (<~>)
  , isoF
  , viewF, reviewF, overF
  , fromF
  , Exchange(..)
  ) where

import           Data.Profunctor
import           Control.Natural
import           Data.Tagged

-- | The type of an isomorphism between two functors.  @f '<~>' g@ means that
-- @f@ and @g@ are isomorphic to each other.
--
-- We can effectively /use/ an @f \<~\> g@ with:
--
-- @
-- 'viewF'   :: (f \<~\> g) -> f a -> g a
-- 'reviewF' :: (f \<~\> g) -> g a -> a a
-- @
--
-- Use 'viewF' to extract the "@f@ to @g@" function, and 'reviewF' to
-- extract the "@g@ to @f@" function.  Reviewing and viewing the same value
-- (or vice versa) leaves the value unchanged.
--
-- One nice thing is that we can compose isomorphisms using '.' from
-- "Prelude":
--
-- @
-- ('.') :: f \<~\> g
--     -> g \<~\> h
--     -> f \<~\> h
-- @
--
-- Another nice thing about this representation is that we have the
-- "identity" isomorphism by using 'id' from "Prelude".
--
-- @
-- 'id' :: f '<~>' g
-- @
--
-- As a convention, most isomorphisms have form "X-ing", where the
-- forwards function is "ing".  For example, we have:
--
-- @
-- 'Data.HBifunctor.Tensor.splittingSF' :: 'Data.HBifunctor.Tensor.Monoidal' t => 'Data.HBifunctor.Associative.SF' t a '<~>' t f ('Data.HBifunctor.Tensor.MF' t f)
-- 'Data.HBifunctor.Tensor.splitSF'     :: Monoidal t => SF t a  '~>' t f (MF t f)
-- @
type f <~> g  = forall p a. Profunctor p => p (g a) (g a) -> p (f a) (f a)
infixr 0 <~>

-- | Create an @f '<~>' g@ by providing both legs of the isomorphism (the
-- @f a -> g a@ and the @g a -> f a@.
isoF
    :: f ~> g
    -> g ~> f
    -> f <~> g
isoF = dimap

-- | Use a '<~>' by retrieving the "forward" function:
--
-- @
-- 'viewF'   :: (f <~> g) -> f a -> g a
-- @
viewF :: f <~> g -> f ~> g
viewF i = runForget (i (Forget id))

-- | Use a '<~>' by retrieving the "backwards" function:
--
-- @
-- 'viewF'   :: (f <~> g) -> f a -> g a
-- @
reviewF :: f <~> g -> g ~> f
reviewF i x = unTagged (i (Tagged x))

-- | Lift a function @g a ~> g a@ to be a function @f a -> f a@, given an
-- isomorphism between the two.
--
-- One neat thing is that @'overF' i id == id@.
overF :: f <~> g -> g ~> g -> f ~> f
overF i f = i f

-- | Reverse an isomorphism.
--
-- @
-- 'viewF'   ('fromF' i) == 'reviewF' i
-- 'reviewF' ('fromF' i) == 'viewF' i
-- @
fromF
    :: f <~> g
    -> g <~> f
fromF i = isoF g f
  where
    Exchange f g = i (Exchange id id)

-- | Profunctor that allows us to implement 'fromF'.
data Exchange a b s t = Exchange (s -> a) (b -> t)
  deriving Functor

instance Profunctor (Exchange a b) where
    dimap f g (Exchange x y) = Exchange (x . f) (g . y)