1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-08-10 22:31:32 +02:00

fix: simplify DI implementation

Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
Carsten Leue
2023-11-10 21:44:24 +01:00
parent 6a8c3b3a21
commit 677392dfce
5 changed files with 191 additions and 233 deletions

View File

@@ -17,8 +17,6 @@
package erasure
import (
"log"
A "github.com/IBM/fp-go/array"
"github.com/IBM/fp-go/errors"
F "github.com/IBM/fp-go/function"
@@ -41,37 +39,61 @@ func itemProviderToMap(p Provider) map[string][]ProviderFactory {
return R.Singleton(p.Provides().Id(), A.Of(p.Factory()))
}
var missingProviderError = F.Flow4(
Dependency.String,
errors.OnSome[string]("no provider for dependency [%s]"),
IOE.Left[any, error],
F.Constant1[InjectableFactory, IOE.IOEither[error, any]],
var (
// missingProviderError returns a ProviderFactory that fails due to a missing dependency
missingProviderError = F.Flow4(
Dependency.String,
errors.OnSome[string]("no provider for dependency [%s]"),
IOE.Left[any, error],
F.Constant1[InjectableFactory, IOE.IOEither[error, any]],
)
emptyMulti any = A.Empty[any]()
// emptyMultiDependency returns a ProviderFactory for an empty, multi dependency
emptyMultiDependency = F.Constant1[Dependency](F.Constant1[InjectableFactory](IOE.Of[error](emptyMulti)))
// handleMissingProvider covers the case of a missing provider. It either
// returns an error or an empty multi value provider
handleMissingProvider = F.Flow2(
F.Ternary(isMultiDependency, emptyMultiDependency, missingProviderError),
F.Constant[ProviderFactory],
)
// mergeItemProviders is a monoid for item provider factories
mergeItemProviders = R.UnionMonoid[string](A.Semigroup[ProviderFactory]())
// mergeProviders is a monoid for provider factories
mergeProviders = R.UnionLastMonoid[string, ProviderFactory]()
// collectItemProviders create a provider map for item providers
collectItemProviders = F.Flow2(
A.FoldMap[Provider](mergeItemProviders)(itemProviderToMap),
R.Map[string](itemProviderFactory),
)
// collectProviders collects non-item providers
collectProviders = F.Flow2(
A.Map(providerToEntry),
R.FromEntries[string, ProviderFactory],
)
// assembleProviders constructs the provider map for item and non-item providers
assembleProviders = F.Flow3(
A.Partition(isItemProvider),
T.Map2(collectProviders, collectItemProviders),
T.Tupled2(mergeProviders.Concat),
)
)
var emptyMulti any = A.Empty[any]()
var emptyMultiDependency = F.Constant1[Dependency](F.Constant1[InjectableFactory](IOE.Of[error](emptyMulti)))
func logEntryExit(name string, token Dependency) func() {
log.Printf("Entry: [%s] -> [%s]:[%s]", name, token.Id(), token.String())
return func() {
log.Printf("Exit: [%s] -> [%s]:[%s]", name, token.Id(), token.String())
}
}
// isMultiDependency tests if a dependency is a container dependency
func isMultiDependency(dep Dependency) bool {
return dep.Type() == Multi
return dep.Flag()&Multi == Multi
}
var handleMissingProvider = F.Flow2(
F.Ternary(isMultiDependency, emptyMultiDependency, missingProviderError),
F.Constant[ProviderFactory],
)
// isItemProvider tests if a provivder provides a single item
func isItemProvider(provider Provider) bool {
return provider.Provides().Type() == Item
return provider.Provides().Flag()&Item == Item
}
// itemProviderFactory combines multiple factories into one, returning an array
@@ -85,29 +107,7 @@ func itemProviderFactory(fcts []ProviderFactory) ProviderFactory {
}
}
var mergeItemProviders = R.UnionMonoid[string](A.Semigroup[ProviderFactory]())
// collectItemProviders create a provider map for item providers
var collectItemProviders = F.Flow2(
A.FoldMap[Provider](mergeItemProviders)(itemProviderToMap),
R.Map[string](itemProviderFactory),
)
// collectProviders collects non-item providers
var collectProviders = F.Flow2(
A.Map(providerToEntry),
R.FromEntries[string, ProviderFactory],
)
var mergeProviders = R.UnionLastMonoid[string, ProviderFactory]()
// assembleProviders constructs the provider map for item and non-item providers
var assembleProviders = F.Flow3(
A.Partition(isItemProvider),
T.Map2(collectProviders, collectItemProviders),
T.Tupled2(mergeProviders.Concat),
)
// MakeInjector creates an [InjectableFactory] based on a set of [Provider]s
func MakeInjector(providers []Provider) InjectableFactory {
type Result = IOE.IOEither[error, any]
@@ -126,8 +126,6 @@ func MakeInjector(providers []Provider) InjectableFactory {
// lazy initialization, so we can cross reference it
injFct = func(token Dependency) Result {
defer logEntryExit("inj", token)()
key := token.Id()
// according to https://github.com/golang/go/issues/44159 this
@@ -135,8 +133,7 @@ func MakeInjector(providers []Provider) InjectableFactory {
actual, loaded := resolved.Load(key)
if !loaded {
computeResult := func() Result {
defer logEntryExit("computeResult", token)()
computeResult := L.MakeLazy(func() Result {
return F.Pipe5(
token,
T.Replicate2[Dependency],
@@ -149,7 +146,7 @@ func MakeInjector(providers []Provider) InjectableFactory {
IG.Ap[ProviderFactory](injFct),
IOE.Memoize[error, any],
)
}
})
actual, _ = resolved.LoadOrStore(key, F.Pipe1(
computeResult,

View File

@@ -21,6 +21,7 @@ import (
A "github.com/IBM/fp-go/array"
E "github.com/IBM/fp-go/either"
F "github.com/IBM/fp-go/function"
I "github.com/IBM/fp-go/identity"
IO "github.com/IBM/fp-go/io"
IOG "github.com/IBM/fp-go/io/generic"
IOE "github.com/IBM/fp-go/ioeither"
@@ -34,10 +35,14 @@ type InjectableFactory = func(Dependency) IOE.IOEither[error, any]
type ProviderFactory = func(InjectableFactory) IOE.IOEither[error, any]
type paramIndex = map[int]int
type paramValue = map[int]any
type handler = func(paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue]
type Provider interface {
fmt.Stringer
// Provides returns the [Dependency] implemented by this provider
Provides() Dependency
// Factory returns s function that can create an instance of the dependency based on an [InjectableFactory]
Factory() ProviderFactory
}
@@ -62,19 +67,67 @@ func MakeProvider(token Dependency, fct ProviderFactory) Provider {
return &provider{token, fct}
}
func mapFromToken(idx int, token Dependency) map[TokenType]paramIndex {
return R.Singleton(token.Type(), R.Singleton(idx, idx))
func mapFromToken(idx int, token Dependency) map[int]paramIndex {
return R.Singleton(token.Flag()&BehaviourMask, R.Singleton(idx, idx))
}
var mergeTokenMaps = R.UnionMonoid[TokenType](R.UnionLastSemigroup[int, int]())
var foldDeps = A.FoldMapWithIndex[Dependency](mergeTokenMaps)(mapFromToken)
var (
mergeTokenMaps = R.UnionMonoid[int](R.UnionLastSemigroup[int, int]())
foldDeps = A.FoldMapWithIndex[Dependency](mergeTokenMaps)(mapFromToken)
mergeMaps = R.UnionLastMonoid[int, any]()
collectParams = R.CollectOrd[any, any](Int.Ord)(F.SK[int, any])
var lookupIdentity = R.Lookup[paramIndex](Identity)
var lookupOption = R.Lookup[paramIndex](Option)
var lookupIOEither = R.Lookup[paramIndex](IOEither)
var lookupIOOption = R.Lookup[paramIndex](IOOption)
handlers = map[int]handler{
Identity: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return F.Pipe1(
mp,
IOE.TraverseRecord[int](getAt(res)),
)
}
},
Option: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return F.Pipe3(
mp,
IOG.TraverseRecord[IO.IO[map[int]E.Either[error, any]], paramIndex](getAt(res)),
IO.Map(R.Map[int](F.Flow2(
E.ToOption[error, any],
F.ToAny[O.Option[any]],
))),
IOE.FromIO[error, paramValue],
)
}
},
IOEither: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return F.Pipe2(
mp,
R.Map[int](F.Flow2(
getAt(res),
F.ToAny[IOE.IOEither[error, any]],
)),
IOE.Of[error, paramValue],
)
}
},
IOOption: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return F.Pipe2(
mp,
R.Map[int](F.Flow3(
getAt(res),
IOO.FromIOEither[error, any],
F.ToAny[IOO.IOOption[any]],
)),
IOE.Of[error, paramValue],
)
}
},
}
)
type Mapping = map[TokenType]paramIndex
type Mapping = map[int]paramIndex
func getAt[T any](ar []T) func(idx int) T {
return func(idx int) T {
@@ -82,158 +135,41 @@ func getAt[T any](ar []T) func(idx int) T {
}
}
type identityResult = IOE.IOEither[error, map[int]any]
func handleIdentity(mp Mapping) func(res []IOE.IOEither[error, any]) identityResult {
onNone := F.Nullary2(R.Empty[int, any], IOE.Of[error, map[int]any])
return func(res []IOE.IOEither[error, any]) identityResult {
return F.Pipe2(
mp,
lookupIdentity,
O.Fold(
onNone,
IOE.TraverseRecord[int](getAt(res)),
),
)
}
}
type optionResult = IO.IO[map[int]O.Option[any]]
func handleOption(mp Mapping) func(res []IOE.IOEither[error, any]) optionResult {
onNone := F.Nullary2(R.Empty[int, O.Option[any]], IO.Of[map[int]O.Option[any]])
return func(res []IOE.IOEither[error, any]) optionResult {
return F.Pipe2(
mp,
lookupOption,
O.Fold(
onNone,
F.Flow2(
IOG.TraverseRecord[IO.IO[map[int]E.Either[error, any]], paramIndex](getAt(res)),
IO.Map(R.Map[int](E.ToOption[error, any])),
),
),
)
}
}
type ioeitherResult = IO.IO[map[int]IOE.IOEither[error, any]]
func handleIOEither(mp Mapping) func(res []IOE.IOEither[error, any]) ioeitherResult {
onNone := F.Nullary2(R.Empty[int, IOE.IOEither[error, any]], IO.Of[map[int]IOE.IOEither[error, any]])
return func(res []IOE.IOEither[error, any]) ioeitherResult {
return F.Pipe2(
mp,
lookupIOEither,
O.Fold(
onNone,
F.Flow2(
R.Map[int](getAt(res)),
IO.Of[map[int]IOE.IOEither[error, any]],
),
),
)
}
}
type iooptionResult = IO.IO[map[int]IOO.IOOption[any]]
func handleIOOption(mp Mapping) func(res []IOE.IOEither[error, any]) iooptionResult {
onNone := F.Nullary2(R.Empty[int, IOO.IOOption[any]], IO.Of[map[int]IOO.IOOption[any]])
return func(res []IOE.IOEither[error, any]) iooptionResult {
return F.Pipe2(
mp,
lookupIOOption,
O.Fold(
onNone,
F.Flow2(
R.Map[int](F.Flow2(
getAt(res),
IOO.FromIOEither[error, any],
)),
IO.Of[map[int]IOO.IOOption[any]],
),
),
)
}
}
var optionMapToAny = R.Map[int](F.ToAny[O.Option[any]])
var ioeitherMapToAny = R.Map[int](F.ToAny[IOE.IOEither[error, any]])
var iooptionMapToAny = R.Map[int](F.ToAny[IOO.IOOption[any]])
var mergeMaps = R.UnionLastMonoid[int, any]()
var collectParams = R.CollectOrd[any, any](Int.Ord)(F.SK[int, any])
func mergeArguments(
identity identityResult,
option optionResult,
ioeither ioeitherResult,
iooption iooptionResult,
) IOE.IOEither[error, []any] {
return F.Pipe2(
A.From(
identity,
F.Pipe2(
option,
IO.Map(optionMapToAny),
IOE.FromIO[error, map[int]any],
),
F.Pipe2(
ioeither,
IO.Map(ioeitherMapToAny),
IOE.FromIO[error, map[int]any],
),
F.Pipe2(
iooption,
IO.Map(iooptionMapToAny),
IOE.FromIO[error, map[int]any],
),
),
IOE.SequenceArray[error, map[int]any],
IOE.Map[error](F.Flow2(
A.Fold(mergeMaps),
collectParams,
)),
func handleMapping(mp Mapping) func(res []IOE.IOEither[error, any]) IOE.IOEither[error, []any] {
preFct := F.Pipe2(
mp,
R.MapWithIndex(func(idx int, p paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return handlers[idx](p)
}),
R.Collect[int](F.SK[int, func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue]]),
)
doFct := F.Flow2(
I.Flap[IOE.IOEither[error, paramValue], []IOE.IOEither[error, any]],
IOE.TraverseArray[error, func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue], paramValue],
)
postFct := IOE.Map[error](F.Flow2(
A.Fold(mergeMaps),
collectParams,
))
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, []any] {
return F.Pipe2(
preFct,
doFct(res),
postFct,
)
}
}
// MakeProviderFactory constructs a [ProviderFactory] based on a set of [Dependency]s and
// a function that accepts the resolved dependencies to return a result
func MakeProviderFactory(
deps []Dependency,
fct func(param ...any) IOE.IOEither[error, any]) ProviderFactory {
mapping := foldDeps(deps)
identity := handleIdentity(mapping)
optional := handleOption(mapping)
ioeither := handleIOEither(mapping)
iooption := handleIOOption(mapping)
f := F.Unvariadic0(fct)
return func(inj InjectableFactory) IOE.IOEither[error, any] {
// resolve all dependencies
resolved := A.MonadMap(deps, inj)
// resolve dependencies
return F.Pipe1(
mergeArguments(
identity(resolved),
optional(resolved),
ioeither(resolved),
iooption(resolved),
),
IOE.Chain(f),
)
}
return F.Flow3(
F.Curry2(A.MonadMap[Dependency, IOE.IOEither[error, any]])(deps),
handleMapping(foldDeps(deps)),
IOE.Chain(F.Unvariadic0(fct)),
)
}

View File

@@ -17,17 +17,16 @@ package erasure
import "fmt"
type TokenType int
const (
Identity TokenType = iota // required dependency
Option // optional dependency
IOEither // lazy and required
IOOption // lazy and optional
Multi // array of implementations
Item // item of a multi token
IOMulti // lazy and multi
Unknown
BehaviourMask = 0x0f
Identity = 0 // required dependency
Option = 1 // optional dependency
IOEither = 2 // lazy and required
IOOption = 3 // lazy and optional
TypeMask = 0xf0
Multi = 1 << 4 // array of implementations
Item = 2 << 4 // item of a multi token
)
// Dependency describes the relationship to a service
@@ -35,8 +34,8 @@ type Dependency interface {
fmt.Stringer
// Id returns a unique identifier for a token that can be used as a cache key
Id() string
// Type returns a tag that identifies the behaviour of the dependency
Type() TokenType
// Flag returns a tag that identifies the behaviour of the dependency
Flag() int
}
func AsDependency[T Dependency](t T) Dependency {

View File

@@ -276,3 +276,29 @@ func TestEmptyItemProvider(t *testing.T) {
assert.Equal(t, E.Of[error](A.Empty[string]()), value)
}
func TestDependencyOnMultiProvider(t *testing.T) {
// define a multi token
injMulti := MakeMultiToken[string]("configs")
// provide some values
v1 := ConstProvider(injMulti.Item(), "Value1")
v2 := ConstProvider(injMulti.Item(), "Value2")
// mix in non-multi values
p1 := ConstProvider(INJ_KEY1, "Value3")
p2 := ConstProvider(INJ_KEY2, "Value4")
fromMulti := func(val string, multi []string) IOE.IOEither[error, string] {
return IOE.Of[error](fmt.Sprintf("Val: %s, Multi: %s", val, multi))
}
p3 := MakeProvider2(INJ_KEY3, INJ_KEY1.Identity(), injMulti.Container().Identity(), fromMulti)
// populate the injector
inj := DIE.MakeInjector(A.From(p1, p2, v1, v2, p3))
r3 := Resolve(INJ_KEY3)
v := r3(inj)()
assert.Equal(t, E.Of[error]("Val: Value3, Multi: [Value1 Value2]"), v)
}

View File

@@ -77,7 +77,7 @@ var genId = makeId()
type token[T any] struct {
name string
id string
typ DIE.TokenType
flag int
toType func(val any) E.Either[error, T]
}
@@ -85,8 +85,8 @@ func (t *token[T]) Id() string {
return t.id
}
func (t *token[T]) Type() DIE.TokenType {
return t.typ
func (t *token[T]) Flag() int {
return t.flag
}
func (t *token[T]) String() string {
@@ -97,7 +97,7 @@ func (t *token[T]) Unerase(val any) E.Either[error, T] {
return t.toType(val)
}
func makeToken[T any](name string, id string, typ DIE.TokenType, unerase func(val any) E.Either[error, T]) Dependency[T] {
func makeToken[T any](name string, id string, typ int, unerase func(val any) E.Either[error, T]) Dependency[T] {
return &token[T]{name, id, typ, unerase}
}
@@ -157,17 +157,17 @@ func MakeMultiToken[T any](name string) MultiInjectionToken[T] {
itemName := fmt.Sprintf("Item[%s]", name)
// container
container := &injectionToken[[]T]{
token[[]T]{containerName, id, DIE.Multi, toContainer},
makeToken[O.Option[[]T]](fmt.Sprintf("Option[%s]", containerName), id, DIE.Unknown, toOptionType(toContainer)),
makeToken[IOE.IOEither[error, []T]](fmt.Sprintf("IOEither[%s]", containerName), id, DIE.IOMulti, toIOEitherType(toContainer)),
makeToken[IOO.IOOption[[]T]](fmt.Sprintf("IOOption[%s]", containerName), id, DIE.Unknown, toIOOptionType(toContainer)),
token[[]T]{containerName, id, DIE.Multi | DIE.Identity, toContainer},
makeToken[O.Option[[]T]](fmt.Sprintf("Option[%s]", containerName), id, DIE.Multi|DIE.Option, toOptionType(toContainer)),
makeToken[IOE.IOEither[error, []T]](fmt.Sprintf("IOEither[%s]", containerName), id, DIE.Multi|DIE.IOEither, toIOEitherType(toContainer)),
makeToken[IOO.IOOption[[]T]](fmt.Sprintf("IOOption[%s]", containerName), id, DIE.Multi|DIE.IOOption, toIOOptionType(toContainer)),
}
// item
item := &injectionToken[T]{
token[T]{itemName, id, DIE.Item, toItem},
makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", itemName), id, DIE.Unknown, toOptionType(toItem)),
makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", itemName), id, DIE.Unknown, toIOEitherType(toItem)),
makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", itemName), id, DIE.Unknown, toIOOptionType(toItem)),
token[T]{itemName, id, DIE.Item | DIE.Identity, toItem},
makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", itemName), id, DIE.Item|DIE.Option, toOptionType(toItem)),
makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", itemName), id, DIE.Item|DIE.IOEither, toIOEitherType(toItem)),
makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", itemName), id, DIE.Item|DIE.IOOption, toIOOptionType(toItem)),
}
// returns the token
return &multiInjectionToken[T]{container, item}