-- | A builder for functions of variable parameters -- -- 'FunctionBuilder' values can be composed, and eventually /rendered/ -- into a __function__ by 'toFunction'. -- -- For example the composition of: -- -- > -- > fb1 :: FunctionBuilder MyMonoid composeMe (Int -> composeMe) -- > fb1 = deferred ... -- > -- > fb2 :: FunctionBuilder MyMonoid composeMe (String -> composeMe) -- > fb2 = deferred ... -- > -- > fb3 :: FunctionBuilder MyMonoid composeMe (Bool -> composeMe) -- > fb3 = deferred ... -- > -- > fb :: FunctionBuilder MyMonoid composeMe (Int -> String -> Bool -> composeMe) -- > fb = fb1 . fb2 . fb3 -- > -- > f :: Int -> String -> Bool -> MyMonoid -- > f = toFunction fb123 -- -- 'FunctionBuilder's are composed via '.' from 'Category' and '<>' from 'Semigroup'. -- -- This module provides 'Functor', 'Applicative', 'Monad', 'Semigroup', 'Monoid' and -- 'Category' instances; -- -- The smart-constructors 'immediate' and 'deferred' create 'FunctionBuilder's -- that either write a hard coded constant to the output 'Monoid' or add a function -- that will be applied to an additional runtime parameter. -- -- Further /glue-code/ is provided to allow changing the underlying Monoid, see 'bind'. -- module Data.FunctionBuilder where import Control.Category import Data.Monoid import Data.Semigroup import Prelude hiding ( id , (.) ) import Data.Tagged -- | A tricky newtype wrapper around a function that carries out a computation -- resulting in a monoidal output value that is passed to a continuation. -- -- Type parameters: -- -- [@acc@] Type of monoidal value that is build from the parameters of the function -- returned by 'toFunction'. -- For example: In a @printf@ style formatting library @acc@ could be 'String'. -- -- [@next@] The /trick-/ parameter that allows composing @FunctionBuilder@s. -- Also note that 'FunctionBuilder's are contravarient in this parameter; -- @next@ is the output of the continuation @acc -> next@, hence this is an -- /input/ from the perspective of the @FunctionBuilder@. -- -- [@f_make_next@] This is usually a function type that returns @next@, -- this is the type of the output function returned by 'toFunction'. -- -- -- A @FunctionBuilder acc next f@ is a newtype wrapper around functions of type -- @(acc -> next) -> f@. -- -- The immediate return value of the function is usually a function type, -- that takes zero or more arguments: @a_0 -> .. -> a_N -> next@. -- -- The @FunctionBuilder@s that 'deferred' returns are polymorphic in @next@. -- And @next@ is the key for composition. -- -- For example: -- -- @ -- fb1 :: FunctionBuilder MyMonoid next (Int -> next) -- fb1 = deferred undefined -- -- fb2 :: FunctionBuilder MyMonoid next (String -> next) -- fb2 = deferred undefined -- -- newtype MyMonoid = MyMonoid () deriving (Semigroup, Monoid) -- @ -- -- When we /desugar/ with ghci: -- -- >>> :t (runFunctionBuilder fb1) -- (runFunctionBuilder fb1) :: (MyMonoid -> next) -> Int -> next -- -- >>> :t (runFunctionBuilder fb2) -- (runFunctionBuilder fb2) :: (MyMonoid -> next) -> String -> next -- -- 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 (see example below). -- -- (2) By using '<>' to append a fixed value to the accumulator directly. -- -- When __composing__ @fb1@ and @fb2@ using '.' we get: -- -- >>> :t (fb1 . fb2) -- (fb1 . fb2) :: FunctionBuilder MyMonoid a (Int -> String -> a) -- -- And desugared: -- -- >>> :t runFunctionBuilder (fb1 . fb2) -- runFunctionBuilder (fb1 . fb2) :: (MyMonoid -> next) -> Int -> String -> next -- -- What happened during composition was that the @next@ in @fb1@ was used to insert -- into @Int -> next@ the @String -> other_next@ from @fb2@; such that this results in -- @Int -> (String -> other_next)@. -- (Note: For clarity I renamed the type local type parameter @next@ to @other_next@ from @fb2@) -- -- Also, there is the 'StaticContent' type class for types that have function builders. -- -- NOTE: @FunctionBuilder w a b@ is actually @Cokleisli ((->) w) a b@ -- When @w@ is a 'Monoid' then, @((->) w)@ is a @Comonad@ instance. -- The reason why this library does not use the comonad library under the hood -- is currently only the missing 'Semigroup' and 'Monoid' instances; and -- to avoid orphan instances a newtype wrapper would be required, and that -- does not justify the additional dependency and the pollution of this library -- with scary @Cokleislies@ and @Comonads@. 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 (f . h) 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 return = pure (FB m) >>= f = FB (\k -> runFunctionBuilder (f (m k)) k) -- | Get the composed __output function__ of a 'FunctionBuilder'. -- -- The 'FunctionBuilder' passed to this function must match this signature: -- -- > FunctionBuilder m m (arg0 -> .. -> m) -- -- This means that the result of the generated function @arg0 -> .. -> m@ __MUST__ be -- @m@, the underlying 'Monoid'. -- -- The 'FunctionBuilder's generated by 'deferred' and 'immediate' are parametric -- in the second type parameter and match the type signature required by this function. -- -- Example 1: -- -- > fb :: FunctionBuilder String String (Int -> Double -> Int -> String) -- > fb = undefined -- > -- > example :: Int -> Double -> Int -> String -- > example = toFunction fb -- -- Example 2: -- -- > 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 -- | A smart constructor for a 'FunctionBuilder' with that will build a function with -- a parameter, and when the generated function is applied at that parameter -- the function given here will be applied to the argument and the resulting monoidal -- value will be appended to the result. -- -- The generated builder can be passed to 'toFunction' since it is parametric -- in its second type parameter. -- -- 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 = deferred show -- -- > example :: (Show a, Show b) => a -> b -> String -- > example = toFunction (showing . showing) -- -- >>> example True 0.33214 -- "True0.33214" -- -- See the example in 'toFunction'. -- -- @since 0.3.0.0 deferred :: (a -> m) -> FunctionBuilder m r (a -> r) deferred f = FB (. f) -- | 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) -- -- The generated builder can be passed to 'toFunction' since it is parametric -- in its second type parameter. -- -- 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 ($ m) -- ** Type Classes Creating 'FunctionBuilder' -- | Types @a@ that can be turned into 'FunctionBuilder's -- for a base monoid @m@. -- -- This is the abstract version of 'StaticContent' and 'DynamicContent' -- -- @since 0.1.2.0 class HasFunctionBuilder m a where -- | Get the function type (if any) of the builder. type ToFunction m a r type ToFunction m a r = r -- | Make a 'FunctionBuilder' from some value. toFunctionBuilder :: a -> FunctionBuilder m r (ToFunction m a r) -- *** Specific Classes 'StaticContent' vs. 'DynamicContent' -- | Types @a@ that can be turned into 'FunctionBuilder's -- for a base monoid @m@. -- -- These type can provide a function to work on the internal monoid, -- -- They can be constructed using 'immediate'. -- -- Of course they can incorporate information __statically known at compile time__ -- or via type class dictionaries (through singletons for instance). -- -- For example: -- -- > instance forall s . (KnownSymbol s) => StaticContent String (Proxy s) where -- > addStaticContent = immediate (symbolVal (Proxy @s)) -- -- -- @since 0.2.0.0 class StaticContent m a where -- | Return a 'FunctionBuilder' that can work on the underlying monoid. addStaticContent :: a -> FunctionBuilder m next next default addStaticContent :: (a ~ m) => a -> FunctionBuilder m next next addStaticContent = immediate -- | Types that have a 'FunctionBuilder' with a runtime @parameter@ -- for a base monoid @m@. -- -- For example: -- If an instance adds an @Int@ parameter, it will define this family instance: -- -- > instance DynamicContent String (Proxy "%i") Int where -- > addParameter _ = deferred -- -- @since 0.2.0.0 class DynamicContent m a parameter | m a -> parameter where -- | Create a 'FunctionBuilder' that adds a parameter to the output function, -- and converts that argument to a value that can be accumulated in the -- resulting monoidal value. addParameter :: a -> FunctionBuilder m next (parameter -> next) default addParameter :: (a ~ (parameter -> m)) => a -> FunctionBuilder m next (parameter -> next) addParameter = deferred -- ** Modifying Parameters of 'FunctionBuilder's -- | Take away a function parameter added with 'addParameter' by /pre -/ applying it to some -- value. -- -- For example: -- -- > intArg :: FunctionBuilder MyMonoid a (Int -> a) -- > intArg = deferred undefined -- > -- > stringArg :: FunctionBuilder MyMonoid a (String -> a) -- > stringArg = deferred undefined -- > -- > twoInt :: FunctionBuilder MyMonoid a (Int -> String -> a) -- > twoInt = intArg . stringArg -- > -- > example :: FunctionBuilder MyMonoid a (String -> a) -- > example = fillParameter twoInt 42 -- -- -- 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. Similar to a monadic bind '>>=' but more flexible sind the underlying -- 'Monoid' may change too, for example: -- -- > intText :: FunctionBuilder Text next (Int -> next) -- > intText = deferred undefined -- > -- > unpackB :: Text -> FunctionBuilder String next next -- > unpackB = immediate . unpack -- > -- > intStr :: FunctionBuilder String next (Int -> next) -- > intStr = intText `bind` unpackB -- bind :: FunctionBuilder m g_next f_g_next -> (m -> FunctionBuilder n next g_next) -> FunctionBuilder n next f_g_next 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))