{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}

-- | This module provides a general-purpose 'Has' class favoring a style in
-- which the components of the dependency injection environment, instead of
-- being bare functions, are themselves records or newtypes containing
-- functions:
--
-- >>> :{
--  newtype Logger d = Logger {log :: String -> d ()}
--  data Repository d = Repository { select :: String -> d [Int], insert :: [Int] -> d () }
--  newtype Controller d = Controller {serve :: Int -> d String}
--  -- A dependency injection environment that contains the components.
--  type Deps :: (Type -> Type) -> Type
--  data Deps m = Deps
--    { logger :: Logger m,
--      repository :: Repository m,
--      controller :: Controller m
--    }
--  instance Has Logger m (Deps m) where dep Deps {logger} = logger
--  instance Has Repository m (Deps m) where dep Deps {repository} = repository
--  instance Has Controller m (Deps m) where dep Deps {controller} = controller
--  :}
--
--
-- In this style, the functions that are \"invoked\" from the environment are
-- actually record field selectors. These selectors guide the 'Has' class to
-- find the correct components in the environment:
--
--
-- >>> :{
--  makeController :: (Has Logger m deps, Has Repository m deps, Monad m) => deps -> Controller m
--  makeController (asCall -> call) =
--    Controller \url -> do
--      call log "I'm going to insert in the db!"
--      call select "select * from ..."
--      call insert [1, 2, 3, 4]
--      return "view"
-- :}
--
--
-- By convention, the DI environment parameter is usually called @deps@. Notice
-- also the use of the (optional) 'asCall' helper.
--
-- If we regard @makeController@ above as a component constructor, 'Has' lets us
-- avoid having to define separate positional parameters for each dependency of the 
-- constructor. Not only that: it also avoids us having to give names to those 
-- parameters, and even having to mention their types (because they are
-- implicit in the record field selectors).
module Dep.Has
  ( -- * A general-purpose Has
    Has (..),
    HasAll,

    -- * call helpers
    asCall,
    pattern Call,
    -- , pattern Dep

    -- * Component defaults
    Dep (..),
  )
where

import Data.Coerce
import Data.Kind
import GHC.Records
import GHC.TypeLits

-- import Control.Monad.Reader
-- import Control.Monad.Dep.Class

-- | A generic \"Has\" class. When partially applied to a parametrizable
-- record-of-functions @r_@, produces a 2-place constraint
-- saying that the environment @deps@ has the record @r_@ with effect monad @m@.
--
-- The constraint can be used on its own, or with "Control.Monad.Dep.Class".
type Has :: ((Type -> Type) -> Type) -> (Type -> Type) -> Type -> Constraint
class Has (r_ :: (Type -> Type) -> Type) (m :: Type -> Type) (deps :: Type) | deps -> m where
  -- |  Given an environment @deps@, produce a record-of-functions parameterized by the environment's effect monad @m@.
  --
  -- The hope is that using a selector function on the resulting record will
  -- fix the record's type without the need for type annotations.
  --
  -- See also <https://chrisdone.com/posts/import-aliases-field-names/ this import alias trick for avoiding name collisions>.
  dep :: deps -> r_ m
  default dep :: (Dep r_, HasField (DefaultFieldName r_) deps u, Coercible u (r_ m)) => deps -> r_ m
  dep deps
deps = coerce :: forall a b. Coercible a b => a -> b
coerce forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall {k} (x :: k) r a. HasField x r a => r -> a
getField @(DefaultFieldName r_) forall a b. (a -> b) -> a -> b
$ deps
deps

-- | When partially applied to a type-level list @rs_@ of parametrizable records-of-functions,
-- produces a 2-place constraint saying that the environment @e@ has all the
-- records @rs_@ with effect monad @m@.
type HasAll :: [(Type -> Type) -> Type] -> (Type -> Type) -> Type -> Constraint
type family HasAll rs_ m e where
  HasAll '[] m e = ()
  HasAll (r_ : rs_) m e = (Has r_ m e, HasAll rs_ m e)

instance Has r_ m (r_ m, b) where
  dep :: (r_ m, b) -> r_ m
dep (r_ m
r, b
_) = r_ m
r

instance Has r_ m (a, r_ m) where
  dep :: (a, r_ m) -> r_ m
dep (a
_, r_ m
r) = r_ m
r

instance Has r_ m (r_ m, b, c) where
  dep :: (r_ m, b, c) -> r_ m
dep (r_ m
r, b
_, c
_) = r_ m
r

instance Has r_ m (a, r_ m, c) where
  dep :: (a, r_ m, c) -> r_ m
dep (a
_, r_ m
r, c
_) = r_ m
r

instance Has r_ m (a, b, r_ m) where
  dep :: (a, b, r_ m) -> r_ m
dep (a
_, b
_, r_ m
r) = r_ m
r

instance Has r_ m (r_ m, b, c, d) where
  dep :: (r_ m, b, c, d) -> r_ m
dep (r_ m
r, b
_, c
_, d
_) = r_ m
r

instance Has r_ m (a, r_ m, c, d) where
  dep :: (a, r_ m, c, d) -> r_ m
dep (a
_, r_ m
r, c
_, d
_) = r_ m
r

instance Has r_ m (a, b, r_ m, d) where
  dep :: (a, b, r_ m, d) -> r_ m
dep (a
_, b
_, r_ m
r, d
_) = r_ m
r

instance Has r_ m (a, b, c, r_ m) where
  dep :: (a, b, c, r_ m) -> r_ m
dep (a
_, b
_, c
_, r_ m
r) = r_ m
r

-- | A record-of-functions @r_@ might play the role of a \"component\"
-- taking part in dependency injection.
--
-- Each function field is then a \"method\". And the record field selectors are functions
-- which take the component and return the method corresponding to that field.
--
-- Given a dependency injection environment, 'asCall' produces a reusable helper
-- that returns the the method corresponding to a field selector, on the condition that
-- the selector's record actually exists in the environment.
--
-- >>> :{
--  data SomeRecord m = SomeRecord { someSelector :: String -> m () }
--  data Deps m = Deps
--    { someRecord :: SomeRecord m
--    }
--  instance Has SomeRecord m (Deps m) where
--    dep (Deps {someRecord}) = someRecord
--  :}
--
--   In practice, this just means that you can write @call someSelector@ instead of
--   @someSelector (dep deps)@:
--
-- >>> :{
--    twoInvocations :: (IO (), IO ())
--    twoInvocations =
--      let deps :: Deps IO = Deps { someRecord = SomeRecord { someSelector = putStrLn } }
--          call = asCall deps
--       in (someSelector (dep deps) "foo", call someSelector "foo")
-- :}
--
--   Using 'asCall' in a view pattern avoids having to name the
--   environment:
--
--
-- >>> :{
--    functionThatCalls :: Has SomeRecord m deps => deps -> m ()
--    functionThatCalls (asCall -> call) = call someSelector "foo"
-- :}
asCall ::
  forall deps m.
  -- | Dependency injection context that contains the components.
  deps ->
  forall r_ method.
  Has r_ m deps =>
  -- | Field selector function that extracts a method from a component.
  (r_ m -> method) ->
  method
asCall :: forall deps (m :: * -> *).
deps
-> forall (r_ :: (* -> *) -> *) method.
   Has r_ m deps =>
   (r_ m -> method) -> method
asCall deps
deps = \r_ m -> method
f -> r_ m -> method
f (forall (r_ :: (* -> *) -> *) (m :: * -> *) deps.
Has r_ m deps =>
deps -> r_ m
dep deps
deps)

-- | Pattern synonym version of 'asCall'. Slightly more succinct and doesn't
-- require @-XViewPatterns@. The synonym is unidirectional: it can only be used for
-- matching.
--
-- >>> :{
--  data SomeRecord m = SomeRecord { someSelector :: String -> m () }
--  data Deps m = Deps
--    { someRecord :: SomeRecord m
--    }
--  instance Has SomeRecord m (Deps m) where
--    dep (Deps {someRecord}) = someRecord
--  functionThatCalls :: Has SomeRecord m deps => deps -> m ()
--  functionThatCalls (Call call) = call someSelector "foo"
-- :}
pattern Call ::
  forall deps m.
  ( forall r_ method.
    Has r_ m deps =>
    (r_ m -> method) ->
    method
  ) ->
  -- | The dependency injection context that we want to match.
  deps
pattern $mCall :: forall {r} {deps} {m :: * -> *}.
deps
-> ((forall (r_ :: (* -> *) -> *) method.
     Has r_ m deps =>
     (r_ m -> method) -> method)
    -> r)
-> ((# #) -> r)
-> r
Call call <- (asCall -> call)

{-# COMPLETE Call #-}

{-# INLINEABLE Call #-}

-- The deprecated Dep typeclass causes trouble here.
-- -- | The @δ@ should be the first argument of the functions we want to invoke.
-- pattern Dep :: forall env m . (forall r_ . Has r_ m env => r_ m) -> env
-- pattern Dep δ <- ((dep :: env -> forall r_ . Has r_ m env => r_ m) -> δ)
-- {-# COMPLETE Dep #-}
-- {-# INLINABLE Dep #-}

-- | Parametrizable records-of-functions can be given an instance of this
-- typeclass to specify the default field name 'Has' expects for the component
-- in the environment record.
--
-- This allows defining 'Has' instances with empty bodies, thanks to
-- @DefaultSignatures@.
type Dep :: ((Type -> Type) -> Type) -> Constraint

{-# DEPRECATED Dep "more intrusive than useful" #-}

class Dep r_ where
  -- The Char kind would be useful here, to lowercase the first letter of the
  -- k type and use it as the default preferred field name.
  type DefaultFieldName r_ :: Symbol

-- $setup
--
-- >>> :set -XTypeApplications
-- >>> :set -XMultiParamTypeClasses
-- >>> :set -XImportQualifiedPost
-- >>> :set -XTemplateHaskell
-- >>> :set -XStandaloneKindSignatures
-- >>> :set -XNamedFieldPuns
-- >>> :set -XFunctionalDependencies
-- >>> :set -XFlexibleContexts
-- >>> :set -XDataKinds
-- >>> :set -XBlockArguments
-- >>> :set -XFlexibleInstances
-- >>> :set -XTypeFamilies
-- >>> :set -XDeriveGeneric
-- >>> :set -XViewPatterns
-- >>> :set -XScopedTypeVariables
-- >>> import Data.Kind
-- >>> import Control.Monad.Dep
-- >>> import GHC.Generics (Generic)