{-|
Module      : Contravariant
Description : Explore contravariant functors.
Copyright   : © Frank Jung, 2024
License     : GPL-3.0-only

This module explores the concept of contravariant functors in Haskell,
contrasting them with the more common covariant functors. It uses the
'MakeString' newtype as a concrete example.

From [Covariance, contravariance, and positive and negative
position](https://www.schoolofhaskell.com/user/commercial/content/covariance-contravariance)
and [What is a contravariant
functor?](https://stackoverflow.com/questions/38034077/what-is-a-contravariant-functor)

The implementation of the function-result 'fmap' and the function-argument
'contramap' are almost exactly the same thing: just function composition
(the '.' operator). The only difference is on which side you compose the
adapter function.

@
fmap :: (r -> s) -> (a -> r) -> (a -> s)
fmap adapter f = adapter . f
fmap adapter = (adapter .)
fmap = (.)

contramap' :: (b -> a) -> (a -> r) -> (b -> r)
contramap' adapter f = f . adapter
contramap' adapter = (. adapter)
contramap' = flip (.)
@

Note that 'contramap'' is not the same as 'contramap' from
'Data.Functor.Contravariant'. You cannot make '(->) r' an actual instance of
'Contravariant' in Haskell code simply because the 'a' is not the last type
parameter of '(->)'. Conceptually it works perfectly well, and you can always
use a newtype wrapper to swap the type parameters and make that an instance
(the contravariant package defines the 'Op' type for exactly this purpose).

-}

module Contravariant
  (
    -- * Types
    MakeString (..)
    -- * Functions
  , plus3ShowInt
  , showInt
  ) where

import           Data.Functor.Contravariant (Contravariant (..))

-- | A wrapper for a function that turns a type into a 'String'.
--
-- This is a classic example of a contravariant functor.
newtype MakeString a = MakeString { forall a. MakeString a -> a -> String
makeString :: a -> String }

-- | Map a function over the input of a 'MakeString'.
instance Contravariant MakeString where
  contramap :: forall a' a. (a' -> a) -> MakeString a -> MakeString a'
contramap a' -> a
f (MakeString a -> String
g) = (a' -> String) -> MakeString a'
forall a. (a -> String) -> MakeString a
MakeString (a -> String
g (a -> String) -> (a' -> a) -> a' -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. a' -> a
f)

-- | A 'MakeString' that uses the 'Show' instance of 'Int'.
--
-- >>> makeString showInt 42
-- "42"
showInt :: MakeString Int
showInt :: MakeString Int
showInt = (Int -> String) -> MakeString Int
forall a. (a -> String) -> MakeString a
MakeString Int -> String
forall a. Show a => a -> String
show

-- | A 'MakeString' that adds 3 to an 'Int' before showing it.
--
-- >>> makeString plus3ShowInt 10
-- "13"
--
-- This is equivalent to:
--
-- @
-- plus3ShowInt = MakeString (show . (+ 3))
-- @
--
-- The 'contramap' function provides a more general way to compose the
-- transformation.
plus3ShowInt :: MakeString Int
plus3ShowInt :: MakeString Int
plus3ShowInt = (Int -> Int) -> MakeString Int -> MakeString Int
forall a' a. (a' -> a) -> MakeString a -> MakeString a'
forall (f :: * -> *) a' a.
Contravariant f =>
(a' -> a) -> f a -> f a'
contramap (Int -> Int -> Int
forall a. Num a => a -> a -> a
+Int
3) MakeString Int
showInt

-- How to deal with newtypes:
--
-- @
-- newtype Mark = MkMark Int deriving Show
-- incMark (MkMark a) = MkMark (a + 1)
-- x = MkMark 12        -- x :: Mark = MkMark 12
-- y = incMark x        -- y :: Mark = MkMark 13
-- @