-- | This library allows you to build function builder libraries.
--
-- Several 'FunctionBuilder' values sharing a common monoidal output type can be composed
-- to a big 'FunctionBuilder' value, in order to build an __output function__ that
-- has a flexible number and types of parameters depending, on the individual
-- 'FunctionBuilder's used. This output function can be obtained by 'toFunction'.
--
-- 'FunctionBuilder's can also be composed via standard type classes.
--
-- This module provides 'Functor', 'Applicative', 'Monad', 'Semigroup', 'Monoid' and
-- 'Category' instances;
--
-- The basic building blocks when generating a poly variadic function
-- are 'immediate' and 'addParameter'.
--
-- The output function is obtained from a 'FunctionBuilder' by __toFunction__.
--
module Data.FunctionBuilder where

import           Control.Category
import           Data.Monoid
import           Data.Semigroup
import           Prelude                 hiding ( id
                                                , (.)
                                                )
import           Data.Tagged

-- | A function, that takes an accumulation function as paramater,
-- and returns a function that will have zero or more parameters and returns
-- an accumulated result: @(acc -> next)
--
-- A @FunctionBuilder acc next f@ is a function @(acc -> next) -> f@.
--
-- Type parameter:
--
-- [@acc@] The final output value that gets build up by the
-- applying the resulting function build by the composed @FunctionBuilder@s.
-- If you were building a @printf@ style library, then @acc@ would
-- probably be 'String'.
--
-- [@next@] The @next@ parameter allows composing @FunctionBuilder@s, and the final output
-- will be a function @f@ with zero or more parameters of different type
-- resulting in an @acc@ value. Most 'FunctionBuilder's are parameteric in @next@ and
-- also have @next@ in a in @f_make_next@.
-- Also note that in @(acc -> next) -> f_make_next@ the @next@ is
-- the output of the continuation @acc -> next@ passed to the @FunctionBuilder@ function,
-- hence this /output/ is actually in /input/ from the perspective of the @FunctionBuilder@,
-- which makes a @FunctionBuilder@ 'Contravariant' in @next@.
--
-- [@f_make_next@] This is usually a function that returns @next@ or is
-- directly @next@, this is the resulting - seemingly /poly variadic/ -
-- __outout function__ composed through the composition of @FunctionBuilder@s, and
-- obtained by 'toFunction'.
--
-- It is required for the type-class instances allowing the
-- composition as 'Semigroup's or 'Monoid's or even 'Category'.
--
-- It is totaly valid to apply it to 'id', to get @f@, and behind @f@
-- typically lies a function of some parameters to @next@.
--
-- At the end of /the chain/ @next@ will be @acc@ and before that
-- the function that takes the next parameters and then returns out.
--
-- See `toFunction`.
--
-- Composition comes in two flavours:
--
--     (1) By using `(.)` to add to the accumulator a value passed to an additional argument
--     of the resulting output function.
--
--     (2) By using `(<>)` to append a fixed value to the accumulator directly.
--
-- For example:
--
-- > import Data.Monoid (Sum(..))
-- >
-- > add :: FunctionBuilder (Sum Int) next (Int -> next)
-- > add = FB $ \k -> \x -> k (Sum x)
--
-- Here the @next@ parameter in @add@ is just passed through and
-- is the __key__ to be able to __compose__ @FunctionBuilder@s. @add@ is
-- parametric in @next@.
-- .
-- And when we are done composing, we pass `id` to the @FunctionBuilder@, which
-- forces the the @next@ parameter to match the @acc@ type, and which
-- would the make @add@ function look like this:
--
-- > addToZero :: FunctionBuilder (Sum Int) (Sum Int) (Int -> Sum Int)
-- > addToZero = add
newtype FunctionBuilder acc next f_make_next = FB {runFunctionBuilder :: (acc -> next) -> f_make_next }

-- | Compose 'FunctionBuilder's such that the output function first takes all parameters
-- from the first 'FunctionBuilder' and then all parameters from the second 'FunctionBuilder' and then
-- appends the results of both functions, which is why we need the 'Monoid' constraint.
instance Monoid m => Category (FunctionBuilder m) where
  (.) (FB f) (FB g) = FB (\k -> (f (\m1 -> g (\m2 -> k (m1 <> m2)))))
  id = FB ($ mempty)

-- | Allow appending a 'FunctionBuilder' to another without changing the resulting output function.
-- For example, 'FunctionBuilder's that have @FunctionBuilder m r r@ can append something to @m@.
-- It is not possible to add new parameters to the output function, this can only be done by
-- the 'Category' instance.
instance Semigroup m => Semigroup (FunctionBuilder m r r) where
  (<>) (FB f) (FB g) = FB (\k -> (f (\m1 -> g (\m2 -> k (m1 <> m2)))))

-- | Allow appending a 'FunctionBuilder' to another without changing the resulting output function.
-- For example, 'FunctionBuilder's that have @FunctionBuilder m r r@ can append something to @m@.
-- It is not possible to add new parameters to the output function, this can only be done by
-- the 'Category' instance.
instance Monoid m => Monoid (FunctionBuilder m r r) where
  mappend = (<>)
  mempty  = id

instance Functor (FunctionBuilder m r) where
  fmap f (FB !h) = FB $ \k -> f (h k)

instance Applicative (FunctionBuilder m r) where
  pure = FB . const
  (FB f) <*> (FB x) = FB (\k -> f k (x k))

instance Monad (FunctionBuilder m r) where
  (FB m) >>= f = FB (\k -> runFunctionBuilder (f (m k)) k)

-- | Turn a 'FunctionBuilder' into the __output function__ that consumes
-- zero or more of parameter and then always return @outout@.
--
-- If passed a 'FunctionBuilder' value of type @FunctionBuilder String String (Int -> Double -> Int -> String)@
--
-- For example:
--
-- > example :: Int -> Double -> Int -> String
-- > example = toFunction (i . d . i)
-- >
-- > s :: String -> FunctionBuilder String a a
-- > s x = FB (\k -> k x)
-- >
-- > i :: FunctionBuilder String next (Int -> next)
-- > i = FB (\k x -> k $ show x)
-- >
-- > d :: FunctionBuilder String next (Double -> next)
-- > d = FB (\k x -> k $ show x)
toFunction :: FunctionBuilder output output make_output -> make_output
toFunction = ($ id) . runFunctionBuilder

-- ** Building 'FunctionBuilder's

-- | Create a 'FunctionBuilder' that /appends/ something to the (monoidal-) output value.
--
-- This is a smart constructor for a 'FunctionBuilder'.
-- This functions is probably equal to:
--
-- > immediate x = FB (\k -> k x)
--
-- Example:
--
-- When building a 'String' formatting 'FunctionBuilder'
-- the function to append a literal string could be:
--
-- > s :: String -> FunctionBuilder String a a
-- > s = immediate
--
-- > c :: Char -> FunctionBuilder String a a
-- > c = immediate . (:[])
--
-- > example :: String
-- > example = toFunction (s "hello" . c ' ' . s "world")
--
-- >>> example
-- "hello world"
--
-- See the example in `toFunction`.
immediate :: m -> FunctionBuilder m r r
immediate m = FB { runFunctionBuilder = ($ m) }

-- | Create a 'FunctionBuilder' that adds an argument to the output function,
-- and converts that argument to a value that can be accumulated in the
-- resulting monoidal value.
--
-- This is a smart constructor for a 'FunctionBuilder'.
-- This functions is probably equal to:
--
-- > addParameter f = FB (\k x -> k (f x))
--
-- Example:
--
-- When building a 'String' formatting 'FunctionBuilder'
-- the function to append a parameter that has a show instance could be:
--
-- > showing :: Show a => FunctionBuilder String r (a -> r)
-- > showing = addParameter show
--
-- > example :: (Show a, Show b) => a -> b -> String
-- > example = toFunction (showing . showing)
--
-- >>> example True 0.33214
-- "True0.33214"
--
-- See the example in `toFunction`.
addParameter :: (a -> m) -> FunctionBuilder m r (a -> r)
addParameter f = FB { runFunctionBuilder = (. f) }

-- ** Modifying Parameters of 'FunctionBuilder's

-- | Take away a function parameter added with 'addParameter' by /pre -/ applying it to some
-- value.
-- This is equivalent to:
--
-- @
--     fillParameter f x = f <*> pure x
-- @
--
fillParameter :: FunctionBuilder m r (a -> b) -> a -> FunctionBuilder m r b
fillParameter (FB !f) x = FB $ \k -> f k x

-- | Convert a 'FunctionBuilder' for a function @(a -> b)@ to @(Tagged tag a -> b)@.
tagParameter
  :: forall tag m r a b
   . FunctionBuilder m r (a -> b)
  -> FunctionBuilder m r (Tagged tag a -> b)
tagParameter = fmap (\f -> f . untag)

-- ** 'FunctionBuilder' Transformations

-- | Compose to 'FunctionBuilder's such that the second 'FunctionBuilder' may depend on the intermediate result
-- of the first. If you skwirm hard enough you __almost__ see '(>>=)' with @m ~ n@.
bind
  :: FunctionBuilder m b c
  -> (m -> FunctionBuilder n a b)
  -> FunctionBuilder n a c
bind mbc fm =
  FB $ \kna -> runFunctionBuilder mbc (($ kna) . runFunctionBuilder . fm)

-- | Convert the accumulated (usually monoidal-) value,
-- this allows to change the underlying accumlator type.
mapAccumulator :: (m -> n) -> FunctionBuilder m a b -> FunctionBuilder n a b
mapAccumulator into (FB f) = FB (\k -> f (k . into))

-- | Convert the output of a 'FunctionBuilder' value; since most
-- 'FunctionBuilder's are parameteric in @r@ they also have @r@ in a
-- in @a@, such that @a@ always either is @r@ or is a
-- function returning @r@ eventually.
--
-- In order to get from a 'FunctionBuilder' that can accept a continuation returning it an @r@
-- to a 'FunctionBuilder' that accepts continuations returning an @s@ instead, we need to
-- apply a function @s -> r@ to the return value of the continuation.
--
-- Note that a 'mapNext' will not only change the @r@ to an @s@ but
-- probably also the the @a@, when it is parametric, as in this contrived example:
--
-- > example :: Int -> x -> Sum Int
-- > example = toFunction (ign add)
-- >
-- > add :: FunctionBuilder (Sum Int) next (Int -> next)
-- > add = FB (\k x -> k $ Sum x)
-- >
-- > ign :: FunctionBuilder m (x -> r) a -> FunctionBuilder m r a
-- > ign = mapNext const
--
-- Here the extra parameter @x@ is /pushed down/ into the @a@ of the @add@ 'FunctionBuilder'.
mapNext :: (s -> r) -> FunctionBuilder m r a -> FunctionBuilder m s a
mapNext outof (FB f) = FB (\k -> f (outof . k))