packages feed

cauldron-0.6.1.0: lib/Cauldron/Builder.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

-- | This module is not required to use 'Cauldron's, but it provides a 'Builder' monad which lets you
-- define them in a manner which more closely resembles the syntax of wiring things \"manually\" in 'IO' or 'Managed'.
--
-- >>> :{
-- data Foo
--   = EndFoo
--   | FooToBar Bar
--   deriving stock (Show)
-- --
-- data Bar
--   = EndBar
--   | BarToFoo Foo
--   deriving stock (Show)
-- --
-- newtype Serializer a = Serializer {runSerializer :: a -> String}
-- --
-- makeFooSerializer :: Serializer Bar -> Serializer Foo
-- makeFooSerializer Serializer {runSerializer = runBar} =
--   Serializer
--     { runSerializer = \case
--         EndFoo -> ".EndFoo"
--         FooToBar bar -> ".FooToBar" ++ runBar bar
--     }
-- --
-- makeBarSerializer :: Serializer Foo -> Serializer Bar
-- makeBarSerializer Serializer {runSerializer = runFoo} =
--   Serializer
--     { runSerializer = \case
--         EndBar -> ".EndBar"
--         BarToFoo foo -> ".BarToFoo" ++ runFoo foo
--     }
-- --
-- builder :: Builder Identity ()
-- builder = mdo
--   foo <- _val_ $ makeFooSerializer <$> bar
--   bar <- _val_ $ makeBarSerializer <$> foo
--   pure ()
-- --
-- cauldron :: Either DuplicateBeans (Cauldron Identity)
-- cauldron = execBuilder builder
-- :}
--
-- Note that in the 'Builder' monad the values that we bind with @<-@ when using
-- functions like 'add', '_val_', or '_eff_' are really 'Args' values which
-- merely carry type information. We can dispense with them and use 'arg' or
-- 'wire' instead:
--
-- >>> :{
-- builder2 :: Builder Identity ()
-- builder2 = mdo
--   _ <- add $ val_ $ makeFooSerializer <$> arg
--   _ <- _val_ $ wire makeBarSerializer
--   pure ()
-- :}
module Cauldron.Builder
  ( Builder,
    add,
    execBuilder,

    -- * Two beans of the same type are forbidden
    DuplicateBeans (..),
    prettyDuplicateBeans,
    prettyDuplicateBeansLines,

    -- * Being polymorphic on the wiring monad
    MonadWiring (..),
    _ioEff_,
  )
where

import Cauldron
import Cauldron.Args
import Cauldron.Managed
import Control.Exception (Exception (..))
import Control.Monad.Fix
import Control.Monad.IO.Class
import Data.Dynamic
import Data.Foldable qualified
import Data.Function ((&))
import Data.Functor.Identity
import Data.Kind
import Data.List qualified
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Sequence (Seq)
import Data.Sequence qualified
import Data.Typeable
import GHC.Exception (CallStack, prettyCallStackLines)
import GHC.Stack (HasCallStack, callStack, withFrozenCallStack)

data Builder m a = Builder (Cauldron m) (Map TypeRep (Seq CallStack)) a
  deriving stock (Functor)

combineCallStackMaps :: Map TypeRep (Seq CallStack) -> Map TypeRep (Seq CallStack) -> Map TypeRep (Seq CallStack)
combineCallStackMaps = Map.unionWith (Data.Sequence.><)

instance Applicative (Builder m) where
  pure a = Builder Cauldron.empty Map.empty a
  Builder c1 m1 f <*> Builder c2 m2 a2 =
    Builder (c1 <> c2) (combineCallStackMaps m1 m2) (f a2)

instance Monad (Builder m) where
  (Builder c1 m1 a) >>= k =
    let Builder c2 m2 r = k a
     in Builder (c1 <> c2) (combineCallStackMaps m1 m2) r

instance MonadFix (Builder m) where
  mfix f =
    let b = f a
        ~(Builder _ _ a) = b
     in b

execBuilder :: Builder m a -> Either DuplicateBeans (Cauldron m)
execBuilder (Builder c m _) =
  let beanDefinitions =
        m
          & Map.mapMaybe \case
            c1 Data.Sequence.:<| c2 Data.Sequence.:<| rest -> Just (c1, c2, Data.Foldable.toList rest)
            _ -> Nothing
   in if (not $ Data.Foldable.null beanDefinitions)
        then Left $ DuplicateBeans beanDefinitions
        else Right c

-- | Because cauldron inject dependencies based on their types, a do-notation block which
-- binds two or more values of the same type would be ambiguous.
--
-- >>> :{
-- builderOops :: Builder Identity ()
-- builderOops = do
--   foo1 <- _val_ $ pure (5 :: Int)
--   foo2 <- _val_ $ pure (6 :: Int)
--   pure ()
-- :}
--
-- >>> :{
-- case execBuilder builderOops of
--    Left (DuplicateBeans _) -> "this should be the result"
--    Right _ -> "won't happen"
-- :}
-- "this should be the result"
data DuplicateBeans = DuplicateBeans (Map TypeRep (CallStack, CallStack, [CallStack]))
  deriving stock (Show)

instance Exception DuplicateBeans where
  displayException = prettyDuplicateBeans

prettyDuplicateBeans :: DuplicateBeans -> String
prettyDuplicateBeans = Data.List.intercalate "\n" . prettyDuplicateBeansLines

prettyDuplicateBeansLines :: DuplicateBeans -> [String]
prettyDuplicateBeansLines (DuplicateBeans beanMap) =
  [ "Some bean types defined more than once in builder:"
  ]
    ++ ( beanMap & Map.foldMapWithKey \rep (c1, c2, rest) ->
           ( [ "- Bean type " ++ show rep ++ " was defined in these locations:"
             ]
               ++ ( (c1 : c2 : rest) & foldMap \location ->
                      (("\t" ++) <$> prettyCallStackLines location)
                  )
           )
       )

-- | Add a 'Recipe' to the 'Cauldron' that is being built.
add ::
  forall {recipelike} {m} (bean :: Type).
  (Typeable bean, ToRecipe recipelike, HasCallStack) =>
  -- | A 'Recipe' or a 'Constructor'.
  recipelike m bean ->
  Builder m (Args bean)
add recipelike =
  Builder
    (Cauldron.empty & Cauldron.insert recipelike)
    (Map.singleton (typeRep (Proxy @bean)) (Data.Sequence.singleton callStack))
    (arg @bean)

-- | This class allows you to define polymorphic \"wirings\" which can work in
-- the 'Builder' monad to produce 'Cauldron's, but also wire beans directly in
-- 'IO' or 'Managed'.
--
-- If we limit ourselves exclusively to the methods of this class, it's not
-- possible to define decorators or secondary beans.
--
-- This class can help migrating from \"direct\"-style wirings to 'Cauldron's.
--
-- >>> :{
-- data A = A deriving Show
-- data B = B deriving Show
-- data C = C deriving Show
-- makeA :: A
-- makeA = A
-- makeB :: A -> B
-- makeB = \_ -> B
-- makeC :: A -> B -> IO C
-- makeC = \_ _ -> pure C
-- instantiations :: (Builder IO (Args C), IO (Identity C))
-- instantiations =
--    let polymorphicWiring = do
--           a <- _val_ $ pure makeA
--           b <- _val_ $ makeB <$> a
--           c <- _ioEff_ $ makeC <$> a <*> b
--           pure c
--     in (polymorphicWiring, polymorphicWiring)
-- :}
class (Monad m, Applicative (ArgsApplicative m), Monad (ConstructorMonad m)) => MonadWiring m where
  -- | Wraps every bean type that we bind using methods of this class.
  -- Will be 'Args' for 'Builder', but simply 'Identity' for 'IO' and 'Managed'.
  type ArgsApplicative m :: Type -> Type

  -- | The monad in which constructors have effects.
  type ConstructorMonad m :: Type -> Type

  _val_ :: (Typeable bean, HasCallStack) => ArgsApplicative m bean -> m (ArgsApplicative m bean)
  _eff_ :: (Typeable bean, HasCallStack) => ArgsApplicative m (ConstructorMonad m bean) -> m (ArgsApplicative m bean)

-- | Like '_eff_', but lifts 'IO' constructor effects into a general 'MonadIO'.
_ioEff_ ::
  (MonadWiring m, MonadIO (ConstructorMonad m), Typeable bean, HasCallStack) =>
  ArgsApplicative m (IO bean) ->
  m (ArgsApplicative m bean)
_ioEff_ args = withFrozenCallStack $ _eff_ $ liftIO <$> args

instance (Monad m) => MonadWiring (Builder m) where
  type ArgsApplicative (Builder m) = Args
  type ConstructorMonad (Builder m) = m
  _val_ :: (Typeable bean, HasCallStack) => Args bean -> Builder m (Args bean)
  _val_ v = withFrozenCallStack $ add (val_ v)
  _eff_ :: (Typeable bean, HasCallStack) => Args (m bean) -> Builder m (Args bean)
  _eff_ action = withFrozenCallStack $ add (eff_ action)

instance MonadWiring IO where
  type ArgsApplicative IO = Identity
  type ConstructorMonad IO = IO
  _val_ :: Identity bean -> IO (Identity bean)
  _val_ = pure
  _eff_ :: Identity (IO bean) -> IO (Identity bean)
  _eff_ = sequence

instance MonadWiring Managed where
  type ArgsApplicative Managed = Identity
  type ConstructorMonad Managed = Managed
  _val_ :: Identity a -> Managed (Identity a)
  _val_ = pure
  _eff_ :: Identity (Managed a) -> Managed (Identity a)
  _eff_ = sequence

-- $setup
-- >>> :set -XBlockArguments
-- >>> :set -XOverloadedLists
-- >>> :set -XLambdaCase
-- >>> :set -XRecursiveDo
-- >>> :set -XDerivingStrategies
-- >>> :set -Wno-incomplete-uni-patterns
-- >>> import Data.Functor.Identity
-- >>> import Data.Function ((&))
-- >>> import Data.Monoid
-- >>> import Data.Either (either)
-- >>> import Control.Exception (throwIO)