Skip to content

Instantly share code, notes, and snippets.

@danidiaz
Last active October 9, 2021 08:21
Show Gist options
  • Save danidiaz/358fdccdef51ad37bbec932631dcc4d2 to your computer and use it in GitHub Desktop.
Save danidiaz/358fdccdef51ad37bbec932631dcc4d2 to your computer and use it in GitHub Desktop.
dependency injection in Haskell with a record-of-functions, but no ReaderT
-- Dependency injection using a record-of-functions, but no ReaderT.
--
-- Download in an empty folder, then run with:
--
-- $ cabal install --lib --package-env . dep-t-0.4.4.0
-- $ runghc Main.hs
--
-- Some interesting aspects:
--
-- - No ReaderT transformer! Just plain functions (wrapped in helper datatypes).
--
-- - Components can be polymorphic on the effect monad.
--
-- - Constructor functions for components don't receive their dependencies as
-- separate positional parameters (as they quickly can get unwieldly). Instead,
-- they receive a single composition context ("cc") parameter and use the Has
-- typeclass from the "dep-t" package to extract each dependency.
--
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
module Main where
import Data.Functor.Identity
-- from "dep-t"
-- https://hackage.haskell.org/package/dep-t-0.4.4.0/docs/Control-Monad-Dep-Has.html
import Control.Monad.Dep.Has (Has(dep), Dep(DefaultFieldName))
import GHC.TypeLits
-- Some type from the model.
data User = User deriving Show
-- A small directed acyclic graph of components.
--
-- The controller component depends on the repository component, and both of
-- them depend on the logger component.
-- component interface
data Logger m = Logger {
logMsg :: String -> m ()
}
instance Dep Logger where
type DefaultFieldName Logger = "_logger"
-- component interface
data UserRepository m = UserRepository {
saveUser :: User -> m (),
findUser :: m User
}
instance Dep UserRepository where
type DefaultFieldName UserRepository = "_userRepository"
-- component interface
data UserController m = UserController {
userEndpoint :: m User
}
instance Dep UserController where
type DefaultFieldName UserController = "_userController"
-- component constructor function tied to IO
makeLoggerIO :: cc -> Logger IO
makeLoggerIO _ = Logger putStrLn
-- component constructor function tied to IO
makeUserRepositoryIO :: Has Logger IO cc => cc -> UserRepository IO
makeUserRepositoryIO cc = UserRepository {
saveUser = \User -> do
logMsg (dep cc) "saving user",
findUser = do
logMsg (dep cc) "finding user"
return User
}
-- constructor function which returns a component that is polymorphic on the
-- effect monad. All the effects are performed through its dependencies.
--
-- This is an example of how to keep your "program logic" pure.
makeUserController :: (Has Logger m cc, Has UserRepository m cc, Monad m)
=> cc -> UserController m
makeUserController cc = UserController {
userEndpoint = do
logMsg (dep cc) "entering endpoint"
user <- findUser (dep cc)
logMsg (dep cc) "exiting endpoint"
return user
}
-- A record of components.
-- Parameterized by "h" which wraps each component, and by "m" the effect monad.
data CompositionContext h m = CompositionContext {
_logger :: h (Logger m),
_userRepository :: h (UserRepository m),
_userController :: h (UserController m)
}
deriving anyclass instance Has Logger m (CompositionContext Identity m)
deriving anyclass instance Has UserRepository m (CompositionContext Identity m)
deriving anyclass instance Has UserController m (CompositionContext Identity m)
-- possible alternative to those standalone derives above
-- import GHC.Records
-- instance (Dep somedep, HasField (DefaultFieldName somedep) (CompositionContext Identity m) (Identity (somedep m)))
-- => Has somedep m (CompositionContext Identity m) where
-- dep e = runIdentity $ getField @(DefaultFieldName somedep) e
-- This is a composition context that is "still being built".
--
-- Its fields are functions from the "completed" context to a component. The constructor
-- functions fit that type—but notice that the constructor functions don't depend
-- explicitly on the CompositionContext. Instead, they use Has constraints.
--
-- This would be a good place to apply some aspect-oriented-programming.
openContext :: CompositionContext ((->) (CompositionContext Identity IO)) IO
openContext = CompositionContext {
_logger = makeLoggerIO,
_userRepository = makeUserRepositoryIO,
_userController = makeUserController
}
-- A bit of tie-the-knot magic that produces a fully built compositon context.
--
-- Instead of writing it for each particular context, this function could be
-- implemented for any context, through Generics.
closeContext :: CompositionContext ((->) (CompositionContext Identity m)) m
-> (CompositionContext Identity m)
closeContext CompositionContext { _logger, _userRepository, _userController } =
let closed =
CompositionContext { -- yay for type-changing update
_logger = pure $ _logger closed,
_userRepository = pure $ _userRepository closed,
_userController = pure $ _userController closed
}
in closed
-- A fully built, ready to be used context.
context :: CompositionContext Identity IO
context = closeContext openContext
main :: IO ()
main = do
user <- userEndpoint (dep context)
print $ user
-- (experiment #2: wrap the functions that build each component in a newtype, for clarity)
-- Dependency injection using a record-of-functions, but no ReaderT.
--
-- Download in an empty folder, then run with:
--
-- $ cabal install --lib --package-env . dep-t-0.4.4.0
-- $ runghc Main.hs
--
-- Some interesting aspects:
--
-- - No ReaderT transformer! Just plain functions (wrapped in helper datatypes).
--
-- - Components can be polymorphic on the effect monad.
--
-- - Constructor functions for components don't receive their dependencies as
-- separate positional parameters (as they quickly can get unwieldly). Instead,
-- they receive a single composition context ("cc") parameter and use the Has
-- typeclass from the "dep-t" package to extract each dependency.
--
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE BlockArguments #-}
module Main where
import Data.Kind
import Data.Functor.Identity
-- from "dep-t"
-- https://hackage.haskell.org/package/dep-t-0.4.4.0/docs/Control-Monad-Dep-Has.html
import Control.Monad.Dep.Has (Has(dep), Dep(DefaultFieldName))
import GHC.TypeLits
-- Some helpers which complement Control.Monad.Dep.Has
type HasAll :: [(Type -> Type) -> Type] -> (Type -> Type) -> Type -> Constraint
type family HasAll xs d e where
HasAll '[] d e = ()
HasAll (x : xs) d e = (Has x d e , HasAll xs d e)
type Constructor :: [(Type -> Type) -> Type] -> ((Type -> Type) -> Type) -> (Type -> Type) -> Type
newtype Constructor deps component d = Constructor { exposeConstructor :: forall cc . HasAll deps d cc => cc -> component d }
--
-- Some type from the model.
data User = User deriving Show
-- A small directed acyclic graph of components.
--
-- The controller component depends on the repository component, and both of
-- them depend on the logger component.
-- component interface
type Logger :: (Type -> Type) -> Type
data Logger m = Logger {
logMsg :: String -> m ()
}
instance Dep Logger where
type DefaultFieldName Logger = "_logger"
-- component interface
data UserRepository m = UserRepository {
saveUser :: User -> m (),
findUser :: m User
}
instance Dep UserRepository where
type DefaultFieldName UserRepository = "_userRepository"
-- component interface
data UserController m = UserController {
userEndpoint :: m User
}
instance Dep UserController where
type DefaultFieldName UserController = "_userController"
-- component constructor function tied to IO
makeLoggerIO :: Constructor '[] Logger IO
makeLoggerIO = Constructor \cc -> Logger putStrLn
-- component constructor function tied to IO
makeUserRepositoryIO :: Constructor '[Logger] UserRepository IO
makeUserRepositoryIO = Constructor \cc -> UserRepository {
saveUser = \User -> do
logMsg (dep cc) "saving user",
findUser = do
logMsg (dep cc) "finding user"
return User
}
-- constructor function which returns a component that is polymorphic on the
-- effect monad. All the effects are performed through its dependencies.
--
-- This is an example of how to keep your "program logic" pure.
makeUserController :: Monad m => Constructor '[Logger, UserRepository] UserController m
makeUserController = Constructor \cc -> UserController {
userEndpoint = do
logMsg (dep cc) "entering endpoint"
user <- findUser (dep cc)
logMsg (dep cc) "exiting endpoint"
return user
}
-- A record of components.
-- Parameterized by "h" which wraps each component, and by "m" the effect monad.
data CompositionContext h m = CompositionContext {
_logger :: h (Logger m),
_userRepository :: h (UserRepository m),
_userController :: h (UserController m)
}
deriving anyclass instance Has Logger m (CompositionContext Identity m)
deriving anyclass instance Has UserRepository m (CompositionContext Identity m)
deriving anyclass instance Has UserController m (CompositionContext Identity m)
-- possible alternative to those standalone derives above
-- import GHC.Records
-- instance (Dep somedep, HasField (DefaultFieldName somedep) (CompositionContext Identity m) (Identity (somedep m)))
-- => Has somedep m (CompositionContext Identity m) where
-- dep e = runIdentity $ getField @(DefaultFieldName somedep) e
-- This is a composition context that is "still being built".
--
-- Its fields are functions from the "completed" context to a component. The constructor
-- functions fit that type—but notice that the constructor functions don't depend
-- explicitly on the CompositionContext. Instead, they use Has constraints.
--
-- This would be a good place to apply some aspect-oriented-programming.
openContext :: CompositionContext ((->) (CompositionContext Identity IO)) IO
openContext = CompositionContext {
_logger = exposeConstructor makeLoggerIO,
_userRepository = exposeConstructor makeUserRepositoryIO,
_userController = exposeConstructor makeUserController
}
-- A bit of tie-the-knot magic that produces a fully built compositon context.
--
-- Instead of writing it for each particular context, this function could be
-- implemented for any context, through Generics.
closeContext :: CompositionContext ((->) (CompositionContext Identity m)) m
-> (CompositionContext Identity m)
closeContext CompositionContext { _logger, _userRepository, _userController } =
let closed =
CompositionContext { -- yay for type-changing update
_logger = pure $ _logger closed,
_userRepository = pure $ _userRepository closed,
_userController = pure $ _userController closed
}
in closed
-- A fully built, ready to be used context.
context :: CompositionContext Identity IO
context = closeContext openContext
main :: IO ()
main = do
user <- userEndpoint (dep context)
print $ user
-- (experiment #3: because writing (dep cc) each time is a bit tiresome,
-- a function "makeCaller" with a RankNType is defined that transforms
-- the cc into a function "call" to be used with component "methods".
-- Instead of "logMsg (dep cc)" now we can use "call logMsg".)
-- Dependency injection using a record-of-functions, but no ReaderT.
--
-- Download in an empty folder, then run with:
--
-- $ cabal install --lib --package-env . dep-t-0.4.4.0
-- $ runghc Main.hs
--
-- Some interesting aspects:
--
-- - No ReaderT transformer! Just plain functions (wrapped in helper datatypes).
--
-- - Components can be polymorphic on the effect monad.
--
-- - Constructor functions for components don't receive their dependencies as
-- separate positional parameters (as they quickly can get unwieldly). Instead,
-- they receive a single composition context ("cc") parameter and use the Has
-- typeclass from the "dep-t" package to extract each dependency.
--
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE PatternSynonyms #-}
module Main where
import Data.Kind
import Data.Functor.Identity
-- from "dep-t"
-- https://hackage.haskell.org/package/dep-t-0.4.4.0/docs/Control-Monad-Dep-Has.html
import Control.Monad.Dep.Has (Has(dep), Dep(DefaultFieldName))
import GHC.TypeLits
-- Some helpers which complement Control.Monad.Dep.Has
type HasAll :: [(Type -> Type) -> Type] -> (Type -> Type) -> Type -> Constraint
type family HasAll xs d e where
HasAll '[] d e = ()
HasAll (x : xs) d e = (Has x d e , HasAll xs d e)
type Constructor :: [(Type -> Type) -> Type] -> ((Type -> Type) -> Type) -> (Type -> Type) -> Type
newtype Constructor deps component d = Constructor { exposeConstructor :: forall cc . HasAll deps d cc => cc -> component d }
--
-- Some type from the model.
data User = User deriving Show
-- A small directed acyclic graph of components.
--
-- The controller component depends on the repository component, and both of
-- them depend on the logger component.
-- component interface
type Logger :: (Type -> Type) -> Type
data Logger m = Logger {
logMsg :: String -> m ()
}
instance Dep Logger where
type DefaultFieldName Logger = "_logger"
-- component interface
data UserRepository m = UserRepository {
saveUser :: User -> m (),
findUser :: m User
}
instance Dep UserRepository where
type DefaultFieldName UserRepository = "_userRepository"
-- component interface
data UserController m = UserController {
userEndpoint :: m User
}
instance Dep UserController where
type DefaultFieldName UserController = "_userController"
-- Takes a composition context and returns a function that takes a field selector for a component
-- and pre-applies it with the component extracted from the context.
-- It seems to require this higher-rank type, otherwise GHC complains.
--
-- This idea is to run this function in a ViewPattern to produce a "call"
-- helper that frees us from having to pass (dep cc) all the time.
makeCaller :: forall cc d . cc -> forall component r. Has component d cc => (component d -> r) -> r
makeCaller cc = \f -> f (dep cc)
-- A twist on the previous function: instead of using it with an explicit
-- ViewPattern, we can hide its application inside an unidirectional pattern
-- synonym, which might be slightly nicer.
-- https://downloads.haskell.org/ghc/latest/docs/html/users_guide/exts/pattern_synonyms.html
-- https://gitlab.haskell.org/ghc/ghc/-/wikis/pattern-synonyms
pattern Call :: forall cc d . (forall component r. Has component d cc => (component d -> r) -> r) -> cc
pattern Call cc <- (makeCaller -> cc)
-- component constructor function tied to IO
makeLoggerIO :: Constructor '[] Logger IO
makeLoggerIO = Constructor \cc -> Logger putStrLn
-- component constructor function tied to IO
makeUserRepositoryIO :: Constructor '[Logger] UserRepository IO
makeUserRepositoryIO = Constructor \(makeCaller -> call) -> UserRepository {
saveUser = \User -> do
call logMsg "saving user",
findUser = do
call logMsg "finding user"
return User
}
-- constructor function which returns a component that is polymorphic on the
-- effect monad. All the effects are performed through its dependencies.
--
-- This is an example of how to keep your "program logic" pure.
makeUserController :: Monad m => Constructor '[Logger, UserRepository] UserController m
makeUserController = Constructor \(Call call) -> UserController { -- here we use the pat synonym
userEndpoint = do
call logMsg "entering endpoint"
user <- call findUser
call logMsg "exiting endpoint"
return user
}
-- A record of components.
-- Parameterized by "h" which wraps each component, and by "m" the effect monad.
data CompositionContext h m = CompositionContext {
_logger :: h (Logger m),
_userRepository :: h (UserRepository m),
_userController :: h (UserController m)
}
deriving anyclass instance Has Logger m (CompositionContext Identity m)
deriving anyclass instance Has UserRepository m (CompositionContext Identity m)
deriving anyclass instance Has UserController m (CompositionContext Identity m)
-- possible alternative to those standalone derives above
-- import GHC.Records
-- instance (Dep somedep, HasField (DefaultFieldName somedep) (CompositionContext Identity m) (Identity (somedep m)))
-- => Has somedep m (CompositionContext Identity m) where
-- dep e = runIdentity $ getField @(DefaultFieldName somedep) e
-- This is a composition context that is "still being built".
--
-- Its fields are functions from the "completed" context to a component. The constructor
-- functions fit that type—but notice that the constructor functions don't depend
-- explicitly on the CompositionContext. Instead, they use Has constraints.
--
-- This would be a good place to apply some aspect-oriented-programming.
openContext :: CompositionContext ((->) (CompositionContext Identity IO)) IO
openContext = CompositionContext {
_logger = exposeConstructor makeLoggerIO,
_userRepository = exposeConstructor makeUserRepositoryIO,
_userController = exposeConstructor makeUserController
}
-- A bit of tie-the-knot magic that produces a fully built compositon context.
--
-- Instead of writing it for each particular context, this function could be
-- implemented for any context, through Generics.
closeContext :: CompositionContext ((->) (CompositionContext Identity m)) m
-> (CompositionContext Identity m)
closeContext CompositionContext { _logger, _userRepository, _userController } =
let closed =
CompositionContext { -- yay for type-changing update
_logger = pure $ _logger closed,
_userRepository = pure $ _userRepository closed,
_userController = pure $ _userController closed
}
in closed
-- A fully built, ready to be used context.
context :: CompositionContext Identity IO
context = closeContext openContext
main :: IO ()
main = do
user <- userEndpoint (dep context)
print $ user
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment