See also this solution that uses the registry library instead of Has
-style classes.
Last active
October 9, 2021 08:21
-
-
Save danidiaz/358fdccdef51ad37bbec932631dcc4d2 to your computer and use it in GitHub Desktop.
dependency injection in Haskell with a record-of-functions, but no ReaderT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- (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 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- (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