From 16f708d69fd7cad5ea2e8d8bbe86000ceace83e7 Mon Sep 17 00:00:00 2001 From: Carsten Leue Date: Thu, 9 Nov 2023 19:44:11 +0100 Subject: [PATCH] fix: add initial DI implementation Signed-off-by: Carsten Leue --- di/erasure/injector.go | 91 ++++++++++------- di/erasure/provider.go | 156 +++++++++++++++++++++-------- di/erasure/token.go | 10 +- di/injector.go | 33 +++++++ di/provider.go | 53 ++++++++-- di/provider_test.go | 209 +++++++++++++++++++++++++++++++++++++-- di/token.go | 85 ++++++++++------ di/utils.go | 62 ++++++++++++ di/utils_test.go | 64 ++++++++++++ erasure/erasure.go | 2 +- option/option.go | 4 + record/generic/record.go | 8 ++ record/record.go | 5 + 13 files changed, 656 insertions(+), 126 deletions(-) create mode 100644 di/injector.go create mode 100644 di/utils.go create mode 100644 di/utils_test.go diff --git a/di/erasure/injector.go b/di/erasure/injector.go index 704b167..d9d8069 100644 --- a/di/erasure/injector.go +++ b/di/erasure/injector.go @@ -17,30 +17,48 @@ package erasure import ( - "fmt" + "log" A "github.com/IBM/fp-go/array" + "github.com/IBM/fp-go/errors" F "github.com/IBM/fp-go/function" I "github.com/IBM/fp-go/identity" + IG "github.com/IBM/fp-go/identity/generic" IOE "github.com/IBM/fp-go/ioeither" + L "github.com/IBM/fp-go/lazy" O "github.com/IBM/fp-go/option" + RIOE "github.com/IBM/fp-go/readerioeither" R "github.com/IBM/fp-go/record" T "github.com/IBM/fp-go/tuple" + + "sync" ) func providerToEntry(p Provider) T.Tuple2[string, ProviderFactory] { return T.MakeTuple2(p.Provides().Id(), p.Factory()) } -func missingProviderError(name string) func() IOE.IOEither[error, any] { - return func() IOE.IOEither[error, any] { - return IOE.Left[any](fmt.Errorf("No provider for dependency [%s]", name)) +var missingProviderError = F.Flow3( + errors.OnSome[string]("no provider for dependency [%s]"), + RIOE.Left[InjectableFactory, any, error], + F.Constant[ProviderFactory], +) + +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()) } } func MakeInjector(providers []Provider) InjectableFactory { type Result = IOE.IOEither[error, any] + type LazyResult = L.Lazy[Result] + + // resolved stores the values resolved so far, key is the string ID + // of the token, value is a lazy result + var resolved sync.Map // provide a mapping for all providers factoryById := F.Pipe2( @@ -48,41 +66,48 @@ func MakeInjector(providers []Provider) InjectableFactory { A.Map(providerToEntry), R.FromEntries[string, ProviderFactory], ) - // the resolved map - var resolved = R.Empty[string, Result]() - // the callback + + // the actual factory, we need lazy initialization var injFct InjectableFactory // lazy initialization, so we can cross reference it - injFct = func(token Token) Result { + injFct = func(token Dependency) Result { - hit := F.Pipe3( - token, - Token.Id, - R.Lookup[Result, string], - I.Ap[O.Option[Result]](resolved), - ) + defer logEntryExit("inj", token)() - provFct := F.Pipe2( - token, - T.Replicate2[Token], - T.Map2(F.Flow3( - Token.Id, - R.Lookup[ProviderFactory, string], - I.Ap[O.Option[ProviderFactory]](factoryById), - ), F.Flow2( - Token.String, - missingProviderError, - )), - ) + key := token.Id() - x := F.Pipe4( - token, - Token.Id, - R.Lookup[Result, string], - I.Ap[O.Option[Result]](resolved), - O.GetOrElse(F.Flow2()), - ) + // according to https://github.com/golang/go/issues/44159 this + // is the best way to use the sync map + actual, loaded := resolved.Load(key) + if !loaded { + + computeResult := func() Result { + defer logEntryExit("computeResult", token)() + return F.Pipe5( + token, + T.Replicate2[Dependency], + T.Map2(F.Flow3( + Dependency.Id, + R.Lookup[ProviderFactory, string], + I.Ap[O.Option[ProviderFactory]](factoryById), + ), F.Flow2( + Dependency.String, + missingProviderError, + )), + T.Tupled2(O.MonadGetOrElse[ProviderFactory]), + IG.Ap[ProviderFactory](injFct), + IOE.Memoize[error, any], + ) + } + + actual, _ = resolved.LoadOrStore(key, F.Pipe1( + computeResult, + L.Memoize[Result], + )) + } + + return actual.(LazyResult)() } return injFct diff --git a/di/erasure/provider.go b/di/erasure/provider.go index 50c2ac8..3cc6956 100644 --- a/di/erasure/provider.go +++ b/di/erasure/provider.go @@ -24,27 +24,29 @@ import ( IO "github.com/IBM/fp-go/io" IOG "github.com/IBM/fp-go/io/generic" IOE "github.com/IBM/fp-go/ioeither" + IOO "github.com/IBM/fp-go/iooption" + Int "github.com/IBM/fp-go/number/integer" O "github.com/IBM/fp-go/option" RIOE "github.com/IBM/fp-go/readerioeither" R "github.com/IBM/fp-go/record" - T "github.com/IBM/fp-go/tuple" ) -type InjectableFactory = RIOE.ReaderIOEither[Token, error, any] +type InjectableFactory = RIOE.ReaderIOEither[Dependency, error, any] type ProviderFactory = RIOE.ReaderIOEither[InjectableFactory, error, any] +type paramIndex = map[int]int type Provider interface { fmt.Stringer - Provides() Token + Provides() Dependency Factory() ProviderFactory } type provider struct { - provides Token + provides Dependency factory ProviderFactory } -func (p *provider) Provides() Token { +func (p *provider) Provides() Dependency { return p.provides } @@ -56,21 +58,23 @@ func (p *provider) String() string { return fmt.Sprintf("Provider for [%s]", p.provides) } -func MakeProvider(token Token, fct ProviderFactory) Provider { +func MakeProvider(token Dependency, fct ProviderFactory) Provider { return &provider{token, fct} } -func mapFromToken(idx int, token Token) map[TokenType]map[int]int { +func mapFromToken(idx int, token Dependency) map[TokenType]paramIndex { return R.Singleton(token.Type(), R.Singleton(idx, idx)) } var mergeTokenMaps = R.UnionMonoid[TokenType](R.UnionLastSemigroup[int, int]()) -var foldDeps = A.FoldMapWithIndex[Token](mergeTokenMaps)(mapFromToken) +var foldDeps = A.FoldMapWithIndex[Dependency](mergeTokenMaps)(mapFromToken) -var lookupMandatory = R.Lookup[map[int]int](Mandatory) -var lookupOption = R.Lookup[map[int]int](Option) +var lookupIdentity = R.Lookup[paramIndex](Identity) +var lookupOption = R.Lookup[paramIndex](Option) +var lookupIOEither = R.Lookup[paramIndex](IOEither) +var lookupIOOption = R.Lookup[paramIndex](IOOption) -type Mapping = map[TokenType]map[int]int +type Mapping = map[TokenType]paramIndex func getAt[T any](ar []T) func(idx int) T { return func(idx int) T { @@ -78,14 +82,16 @@ func getAt[T any](ar []T) func(idx int) T { } } -func handleMandatory(mp Mapping) func(res []IOE.IOEither[error, any]) IOE.IOEither[error, map[int]any] { +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]) IOE.IOEither[error, map[int]any] { + return func(res []IOE.IOEither[error, any]) identityResult { return F.Pipe2( mp, - lookupMandatory, + lookupIdentity, O.Fold( onNone, IOE.TraverseRecord[int](getAt(res)), @@ -94,11 +100,13 @@ func handleMandatory(mp Mapping) func(res []IOE.IOEither[error, any]) IOE.IOEith } } -func handleOption(mp Mapping) func(res []IOE.IOEither[error, any]) IO.IO[map[int]O.Option[any]] { +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]) IO.IO[map[int]O.Option[any]] { + return func(res []IOE.IOEither[error, any]) optionResult { return F.Pipe2( mp, @@ -106,7 +114,7 @@ func handleOption(mp Mapping) func(res []IOE.IOEither[error, any]) IO.IO[map[int O.Fold( onNone, F.Flow2( - IOG.TraverseRecord[IO.IO[map[int]E.Either[error, any]], map[int]int](getAt(res)), + IOG.TraverseRecord[IO.IO[map[int]E.Either[error, any]], paramIndex](getAt(res)), IO.Map(R.Map[int](E.ToOption[error, any])), ), ), @@ -114,44 +122,103 @@ func handleOption(mp Mapping) func(res []IOE.IOEither[error, any]) IO.IO[map[int } } -func mergeArguments(count int) func( - mandatory IOE.IOEither[error, map[int]any], - optonal IO.IO[map[int]O.Option[any]], -) IOE.IOEither[error, []any] { +type ioeitherResult = IO.IO[map[int]IOE.IOEither[error, any]] - optMapToAny := R.Map[int](F.ToAny[O.Option[any]]) - mergeMaps := R.UnionLastMonoid[int, any]() +func handleIOEither(mp Mapping) func(res []IOE.IOEither[error, any]) ioeitherResult { - return func( - mandatory IOE.IOEither[error, map[int]any], - optional IO.IO[map[int]O.Option[any]], - ) IOE.IOEither[error, []any] { + onNone := F.Nullary2(R.Empty[int, IOE.IOEither[error, any]], IO.Of[map[int]IOE.IOEither[error, any]]) - return F.Pipe1( - IOE.SequenceT2(mandatory, IOE.FromIO[error](optional)), - IOE.Map[error](T.Tupled2(func(mnd map[int]any, opt map[int]O.Option[any]) []any { - // merge all parameters - merged := mergeMaps.Concat(mnd, optMapToAny(opt)) + return func(res []IOE.IOEither[error, any]) ioeitherResult { - return R.ReduceWithIndex(func(idx int, res []any, value any) []any { - res[idx] = value - return res - }, make([]any, count))(merged) - })), + 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 MakeProviderFactory( - deps []Token, + deps []Dependency, fct func(param ...any) IOE.IOEither[error, any]) ProviderFactory { mapping := foldDeps(deps) - mandatory := handleMandatory(mapping) + identity := handleIdentity(mapping) optional := handleOption(mapping) - - merge := mergeArguments(A.Size(deps)) + ioeither := handleIOEither(mapping) + iooption := handleIOOption(mapping) f := F.Unvariadic0(fct) @@ -160,7 +227,12 @@ func MakeProviderFactory( resolved := A.MonadMap(deps, inj) // resolve dependencies return F.Pipe1( - merge(mandatory(resolved), optional(resolved)), + mergeArguments( + identity(resolved), + optional(resolved), + ioeither(resolved), + iooption(resolved), + ), IOE.Chain(f), ) } diff --git a/di/erasure/token.go b/di/erasure/token.go index 8f0fe9f..de86d20 100644 --- a/di/erasure/token.go +++ b/di/erasure/token.go @@ -20,14 +20,20 @@ import "fmt" type TokenType int const ( - Mandatory TokenType = iota + Identity TokenType = iota Option IOEither IOOption ) -type Token interface { +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 } + +func AsDependency[T Dependency](t T) Dependency { + return t +} diff --git a/di/injector.go b/di/injector.go new file mode 100644 index 0000000..d65f8c5 --- /dev/null +++ b/di/injector.go @@ -0,0 +1,33 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package di + +import ( + DIE "github.com/IBM/fp-go/di/erasure" + F "github.com/IBM/fp-go/function" + IG "github.com/IBM/fp-go/identity/generic" + IOE "github.com/IBM/fp-go/ioeither" + RIOE "github.com/IBM/fp-go/readerioeither" +) + +// Resolve performs a type safe resolution of a dependency +func Resolve[T any](token InjectionToken[T]) RIOE.ReaderIOEither[DIE.InjectableFactory, error, T] { + return F.Flow2( + IG.Ap[DIE.InjectableFactory](DIE.AsDependency(token)), + IOE.ChainEitherK(toType[T]()), + ) +} diff --git a/di/provider.go b/di/provider.go index d513031..bc889eb 100644 --- a/di/provider.go +++ b/di/provider.go @@ -18,18 +18,17 @@ import ( A "github.com/IBM/fp-go/array" DIE "github.com/IBM/fp-go/di/erasure" E "github.com/IBM/fp-go/either" - ER "github.com/IBM/fp-go/erasure" "github.com/IBM/fp-go/errors" F "github.com/IBM/fp-go/function" IOE "github.com/IBM/fp-go/ioeither" T "github.com/IBM/fp-go/tuple" ) -func lookupAt[T any](idx int) func(params []any) E.Either[error, T] { +func lookupAt[T any](idx int, token Dependency[T]) func(params []any) E.Either[error, T] { return F.Flow3( A.Lookup[any](idx), E.FromOption[any](errors.OnNone("No parameter at position %d", idx)), - E.Chain(ER.SafeUnerase[T]), + E.Chain(token.Unerase), ) } @@ -37,21 +36,39 @@ func eraseProviderFactory0[R any](f func() IOE.IOEither[error, R]) func(params . return func(params ...any) IOE.IOEither[error, any] { return F.Pipe1( f(), - IOE.Map[error](ER.Erase[R]), + IOE.Map[error](F.ToAny[R]), ) } } func eraseProviderFactory1[T1 any, R any]( + d1 Dependency[T1], f func(T1) IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] { ft := T.Tupled1(f) - t1 := lookupAt[T1](0) + t1 := lookupAt[T1](0, d1) return func(params ...any) IOE.IOEither[error, any] { return F.Pipe3( E.SequenceT1(t1(params)), IOE.FromEither[error, T.Tuple1[T1]], IOE.Chain(ft), - IOE.Map[error](ER.Erase[R]), + IOE.Map[error](F.ToAny[R]), + ) + } +} + +func eraseProviderFactory2[T1, T2 any, R any]( + d1 Dependency[T1], + d2 Dependency[T2], + f func(T1, T2) IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] { + ft := T.Tupled2(f) + t1 := lookupAt[T1](0, d1) + t2 := lookupAt[T2](1, d2) + return func(params ...any) IOE.IOEither[error, any] { + return F.Pipe3( + E.SequenceT2(t1(params), t2(params)), + IOE.FromEither[error, T.Tuple2[T1, T2]], + IOE.Chain(ft), + IOE.Map[error](F.ToAny[R]), ) } } @@ -63,7 +80,7 @@ func MakeProvider0[R any]( return DIE.MakeProvider( token, DIE.MakeProviderFactory( - A.Empty[DIE.Token](), + A.Empty[DIE.Dependency](), eraseProviderFactory0(fct), ), ) @@ -71,15 +88,31 @@ func MakeProvider0[R any]( func MakeProvider1[T1, R any]( token InjectionToken[R], - d1 InjectionToken[T1], + d1 Dependency[T1], fct func(T1) IOE.IOEither[error, R], ) DIE.Provider { return DIE.MakeProvider( token, DIE.MakeProviderFactory( - A.From[DIE.Token](d1), - eraseProviderFactory1(fct), + A.From[DIE.Dependency](d1), + eraseProviderFactory1(d1, fct), + ), + ) +} + +func MakeProvider2[T1, T2, R any]( + token InjectionToken[R], + d1 Dependency[T1], + d2 Dependency[T2], + fct func(T1, T2) IOE.IOEither[error, R], +) DIE.Provider { + + return DIE.MakeProvider( + token, + DIE.MakeProviderFactory( + A.From[DIE.Dependency](d1, d2), + eraseProviderFactory2(d1, d2, fct), ), ) } diff --git a/di/provider_test.go b/di/provider_test.go index bb9f53c..016f889 100644 --- a/di/provider_test.go +++ b/di/provider_test.go @@ -17,22 +17,217 @@ package di import ( "fmt" "testing" + "time" + A "github.com/IBM/fp-go/array" + DIE "github.com/IBM/fp-go/di/erasure" + E "github.com/IBM/fp-go/either" F "github.com/IBM/fp-go/function" IOE "github.com/IBM/fp-go/ioeither" + O "github.com/IBM/fp-go/option" + "github.com/stretchr/testify/assert" ) -func staticValue(value string) func() IOE.IOEither[error, string] { - return F.Constant(IOE.Of[error](value)) -} - var ( - INJ_KEY1 = MakeToken[string]("INJ_KEY1") INJ_KEY2 = MakeToken[string]("INJ_KEY2") + INJ_KEY1 = MakeToken[string]("INJ_KEY1") + INJ_KEY3 = MakeToken[string]("INJ_KEY3") ) func TestSimpleProvider(t *testing.T) { - p1 := MakeProvider0(INJ_KEY1, staticValue("Carsten")) - fmt.Println(p1) + var staticCount int + + staticValue := func(value string) func() IOE.IOEither[error, string] { + return func() IOE.IOEither[error, string] { + return func() E.Either[error, string] { + staticCount++ + return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now())) + } + } + } + + var dynamicCount int + + dynamicValue := func(value string) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + dynamicCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) + } + } + + p1 := MakeProvider0(INJ_KEY1, staticValue("Carsten")) + p2 := MakeProvider1(INJ_KEY2, INJ_KEY1.Identity(), dynamicValue) + + inj := DIE.MakeInjector(A.From(p1, p2)) + + i1 := Resolve(INJ_KEY1) + i2 := Resolve(INJ_KEY2) + + res := IOE.SequenceT4( + i2(inj), + i1(inj), + i2(inj), + i1(inj), + ) + + r := res() + + assert.True(t, E.IsRight(r)) + assert.Equal(t, 1, staticCount) + assert.Equal(t, 1, dynamicCount) +} + +func TestOptionalProvider(t *testing.T) { + + var staticCount int + + staticValue := func(value string) func() IOE.IOEither[error, string] { + return func() IOE.IOEither[error, string] { + return func() E.Either[error, string] { + staticCount++ + return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now())) + } + } + } + + var dynamicCount int + + dynamicValue := func(value O.Option[string]) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + dynamicCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) + } + } + + p1 := MakeProvider0(INJ_KEY1, staticValue("Carsten")) + p2 := MakeProvider1(INJ_KEY2, INJ_KEY1.Option(), dynamicValue) + + inj := DIE.MakeInjector(A.From(p1, p2)) + + i1 := Resolve(INJ_KEY1) + i2 := Resolve(INJ_KEY2) + + res := IOE.SequenceT4( + i2(inj), + i1(inj), + i2(inj), + i1(inj), + ) + + r := res() + + assert.True(t, E.IsRight(r)) + assert.Equal(t, 1, staticCount) + assert.Equal(t, 1, dynamicCount) +} + +func TestOptionalProviderMissingDependency(t *testing.T) { + + var dynamicCount int + + dynamicValue := func(value O.Option[string]) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + dynamicCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) + } + } + + p2 := MakeProvider1(INJ_KEY2, INJ_KEY1.Option(), dynamicValue) + + inj := DIE.MakeInjector(A.From(p2)) + + i2 := Resolve(INJ_KEY2) + + res := IOE.SequenceT2( + i2(inj), + i2(inj), + ) + + r := res() + + assert.True(t, E.IsRight(r)) + assert.Equal(t, 1, dynamicCount) +} + +func TestProviderMissingDependency(t *testing.T) { + + var dynamicCount int + + dynamicValue := func(value string) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + dynamicCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) + } + } + + p2 := MakeProvider1(INJ_KEY2, INJ_KEY1.Identity(), dynamicValue) + + inj := DIE.MakeInjector(A.From(p2)) + + i2 := Resolve(INJ_KEY2) + + res := IOE.SequenceT2( + i2(inj), + i2(inj), + ) + + r := res() + + assert.True(t, E.IsLeft(r)) + assert.Equal(t, 0, dynamicCount) +} + +func TestEagerAndLazyProvider(t *testing.T) { + + var staticCount int + + staticValue := func(value string) func() IOE.IOEither[error, string] { + return func() IOE.IOEither[error, string] { + return func() E.Either[error, string] { + staticCount++ + return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now())) + } + } + } + + var dynamicCount int + + dynamicValue := func(value string) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + dynamicCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) + } + } + + var lazyEagerCount int + + lazyEager := func(laz IOE.IOEither[error, string], eager string) IOE.IOEither[error, string] { + return F.Pipe1( + laz, + IOE.Chain(func(lazValue string) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + lazyEagerCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s], [%s] at [%s]", lazValue, eager, time.Now())) + } + }), + ) + } + + p1 := MakeProvider0(INJ_KEY1, staticValue("Carsten")) + p2 := MakeProvider1(INJ_KEY2, INJ_KEY1.Identity(), dynamicValue) + p3 := MakeProvider2(INJ_KEY3, INJ_KEY2.IOEither(), INJ_KEY1.Identity(), lazyEager) + + inj := DIE.MakeInjector(A.From(p1, p2, p3)) + + i3 := Resolve(INJ_KEY3) + + r := i3(inj)() + + fmt.Println(r) + + assert.True(t, E.IsRight(r)) + assert.Equal(t, 1, staticCount) + assert.Equal(t, 1, dynamicCount) + assert.Equal(t, 1, lazyEagerCount) } diff --git a/di/token.go b/di/token.go index 1bbc346..96e44d2 100644 --- a/di/token.go +++ b/di/token.go @@ -21,37 +21,56 @@ import ( "sync/atomic" DIE "github.com/IBM/fp-go/di/erasure" + E "github.com/IBM/fp-go/either" IO "github.com/IBM/fp-go/io" IOE "github.com/IBM/fp-go/ioeither" IOO "github.com/IBM/fp-go/iooption" O "github.com/IBM/fp-go/option" ) -type Token[T any] interface { - DIE.Token - ToType(any) O.Option[T] +// Dependency describes the relationship to a service, that has a type and +// a behaviour such as required, option or lazy +type Dependency[T any] interface { + DIE.Dependency + // Unerase converts a value with erased type signature into a strongly typed value + Unerase(val any) E.Either[error, T] } +// InjectionToken uniquely identifies a dependency by giving it an Id, Type and name type InjectionToken[T any] interface { - Token[T] - Option() Token[O.Option[T]] - IOEither() Token[IOE.IOEither[error, T]] - IOOption() Token[IOO.IOOption[T]] + Dependency[T] + // Identity idenifies this dependency as a mandatory, required dependency, it will be resolved eagerly and injected as `T`. + // If the dependency cannot be resolved, the resolution process fails + Identity() Dependency[T] + // Option identifies this dependency as optional, it will be resolved eagerly and injected as `O.Option[T]`. + // If the dependency cannot be resolved, the resolution process continues and the dependency is represented as `O.None[T]` + Option() Dependency[O.Option[T]] + // IOEither identifies this dependency as mandatory but it will be resolved lazily as a `IOE.IOEither[error, T]`. This + // value is memoized to make sure the dependency is a singleton. + // If the dependency cannot be resolved, the resolution process fails + IOEither() Dependency[IOE.IOEither[error, T]] + // IOOption identifies this dependency as optional but it will be resolved lazily as a `IOO.IOOption[T]`. This + // value is memoized to make sure the dependency is a singleton. + // If the dependency cannot be resolved, the resolution process continues and the dependency is represented as the none value. + IOOption() Dependency[IOO.IOOption[T]] } +// makeID creates a generator of unique string IDs func makeId() IO.IO[string] { - var count int64 - return func() string { - return strconv.FormatInt(atomic.AddInt64(&count, 1), 16) - } + var count atomic.Int64 + return IO.MakeIO(func() string { + return strconv.FormatInt(count.Add(1), 16) + }) } +// genId is the common generator of unique string IDs var genId = makeId() type token[T any] struct { - name string - id string - typ DIE.TokenType + name string + id string + typ DIE.TokenType + toType func(val any) E.Either[error, T] } func (t *token[T]) Id() string { @@ -62,44 +81,48 @@ func (t *token[T]) Type() DIE.TokenType { return t.typ } -func (t *token[T]) ToType(value any) O.Option[T] { - return O.ToType[T](value) -} - func (t *token[T]) String() string { return t.name } -func makeToken[T any](name string, id string, typ DIE.TokenType) Token[T] { - return &token[T]{name, id, typ} +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] { + return &token[T]{name, id, typ, unerase} } type injectionToken[T any] struct { token[T] - option Token[O.Option[T]] - ioeither Token[IOE.IOEither[error, T]] - iooption Token[IOO.IOOption[T]] + option Dependency[O.Option[T]] + ioeither Dependency[IOE.IOEither[error, T]] + iooption Dependency[IOO.IOOption[T]] } -func (i *injectionToken[T]) Option() Token[O.Option[T]] { +func (i *injectionToken[T]) Identity() Dependency[T] { + return i +} + +func (i *injectionToken[T]) Option() Dependency[O.Option[T]] { return i.option } -func (i *injectionToken[T]) IOEither() Token[IOE.IOEither[error, T]] { +func (i *injectionToken[T]) IOEither() Dependency[IOE.IOEither[error, T]] { return i.ioeither } -func (i *injectionToken[T]) IOOption() Token[IOO.IOOption[T]] { +func (i *injectionToken[T]) IOOption() Dependency[IOO.IOOption[T]] { return i.iooption } -// MakeToken create a unique injection token for a specific type +// MakeToken create a unique `InjectionToken` for a specific type func MakeToken[T any](name string) InjectionToken[T] { id := genId() return &injectionToken[T]{ - token[T]{name, id, DIE.Mandatory}, - makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", name), id, DIE.Option), - makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEither), - makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOption), + token[T]{name, id, DIE.Identity, toType[T]()}, + makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", name), id, DIE.Option, toOptionType[T]()), + makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEither, toIOEitherType[T]()), + makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOption, toIOOptionType[T]()), } } diff --git a/di/utils.go b/di/utils.go new file mode 100644 index 0000000..47f88d8 --- /dev/null +++ b/di/utils.go @@ -0,0 +1,62 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package di + +import ( + E "github.com/IBM/fp-go/either" + "github.com/IBM/fp-go/errors" + F "github.com/IBM/fp-go/function" + IOE "github.com/IBM/fp-go/ioeither" + IOO "github.com/IBM/fp-go/iooption" + O "github.com/IBM/fp-go/option" +) + +// toType converts and any to a T +func toType[T any]() func(t any) E.Either[error, T] { + return E.ToType[T](errors.OnSome[any]("Value of type [%T] cannot be converted.")) +} + +// toOptionType converts an any to an Option[any] and then to an Option[T] +func toOptionType[T any]() func(t any) E.Either[error, O.Option[T]] { + return F.Flow2( + toType[O.Option[any]](), + E.Chain(O.Fold( + F.Nullary2(O.None[T], E.Of[error, O.Option[T]]), + F.Flow2( + toType[T](), + E.Map[error](O.Of[T]), + ), + )), + ) +} + +// toIOEitherType converts an any to an IOEither[error, any] and then to an IOEither[error, T] +func toIOEitherType[T any]() func(t any) E.Either[error, IOE.IOEither[error, T]] { + return F.Flow2( + toType[IOE.IOEither[error, any]](), + E.Map[error](IOE.ChainEitherK(toType[T]())), + ) +} + +// toIOOptionType converts an any to an IOOption[any] and then to an IOOption[T] +func toIOOptionType[T any]() func(t any) E.Either[error, IOO.IOOption[T]] { + return F.Flow2( + toType[IOO.IOOption[any]](), + E.Map[error](IOO.ChainOptionK(F.Flow2( + toType[T](), + E.ToOption[error, T], + ))), + ) +} diff --git a/di/utils_test.go b/di/utils_test.go new file mode 100644 index 0000000..5ed5694 --- /dev/null +++ b/di/utils_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package di + +import ( + "testing" + + E "github.com/IBM/fp-go/either" + F "github.com/IBM/fp-go/function" + IOE "github.com/IBM/fp-go/ioeither" + O "github.com/IBM/fp-go/option" + "github.com/stretchr/testify/assert" +) + +func TestToType(t *testing.T) { + // good cases + assert.Equal(t, E.Of[error](10), toType[int]()(any(10))) + assert.Equal(t, E.Of[error]("Carsten"), toType[string]()(any("Carsten"))) + assert.Equal(t, E.Of[error](O.Of("Carsten")), toType[O.Option[string]]()(any(O.Of("Carsten")))) + assert.Equal(t, E.Of[error](O.Of(any("Carsten"))), toType[O.Option[any]]()(any(O.Of(any("Carsten"))))) + // failure + assert.False(t, E.IsRight(toType[int]()(any("Carsten")))) + assert.False(t, E.IsRight(toType[O.Option[string]]()(O.Of(any("Carsten"))))) +} + +func TestToOptionType(t *testing.T) { + // good cases + assert.Equal(t, E.Of[error](O.Of(10)), toOptionType[int]()(any(O.Of(any(10))))) + assert.Equal(t, E.Of[error](O.Of("Carsten")), toOptionType[string]()(any(O.Of(any("Carsten"))))) + // bad cases + assert.False(t, E.IsRight(toOptionType[int]()(any(10)))) + assert.False(t, E.IsRight(toOptionType[int]()(any(O.Of(10))))) +} + +func invokeIOEither[T any](e E.Either[error, IOE.IOEither[error, T]]) E.Either[error, T] { + return F.Pipe1( + e, + E.Chain(func(ioe IOE.IOEither[error, T]) E.Either[error, T] { + return ioe() + }), + ) +} + +func TestToIOEitherType(t *testing.T) { + // good cases + assert.Equal(t, E.Of[error](10), invokeIOEither(toIOEitherType[int]()(any(IOE.Of[error](any(10)))))) + assert.Equal(t, E.Of[error]("Carsten"), invokeIOEither(toIOEitherType[string]()(any(IOE.Of[error](any("Carsten")))))) + // bad cases + assert.False(t, E.IsRight(invokeIOEither(toIOEitherType[string]()(any(IOE.Of[error](any(10))))))) + assert.False(t, E.IsRight(invokeIOEither(toIOEitherType[string]()(any(IOE.Of[error]("Carsten")))))) + assert.False(t, E.IsRight(invokeIOEither(toIOEitherType[string]()(any("Carsten"))))) +} diff --git a/erasure/erasure.go b/erasure/erasure.go index c939592..1ea8383 100644 --- a/erasure/erasure.go +++ b/erasure/erasure.go @@ -35,7 +35,7 @@ func Unerase[T any](t any) T { func SafeUnerase[T any](t any) E.Either[error, T] { return F.Pipe2( t, - E.ToType[*T](errors.OnSome[any]("Value %T is not unerased")), + E.ToType[*T](errors.OnSome[any]("Value of type [%T] is not erased")), E.Map[error](F.Deref[T]), ) } diff --git a/option/option.go b/option/option.go index dc43b57..d451898 100644 --- a/option/option.go +++ b/option/option.go @@ -82,6 +82,10 @@ func Fold[A, B any](onNone func() B, onSome func(a A) B) func(ma Option[A]) B { } } +func MonadGetOrElse[A any](fa Option[A], onNone func() A) A { + return MonadFold(fa, onNone, F.Identity[A]) +} + func GetOrElse[A any](onNone func() A) func(Option[A]) A { return Fold(onNone, F.Identity[A]) } diff --git a/record/generic/record.go b/record/generic/record.go index 1ff59ea..a0d2b2f 100644 --- a/record/generic/record.go +++ b/record/generic/record.go @@ -99,6 +99,14 @@ func Collect[M ~map[K]V, GR ~[]R, K comparable, V, R any](f func(K, V) R) func(M return F.Bind2nd(collect[M, GR, K, V, R], f) } +func CollectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K]) func(f func(K, V) R) func(M) GR { + return func(f func(K, V) R) func(M) GR { + return func(r M) GR { + return collectOrd[M, GR](o, r, f) + } + } +} + func Reduce[M ~map[K]V, K comparable, V, R any](f func(R, V) R, initial R) func(M) R { return func(r M) R { return G.Reduce(r, f, initial) diff --git a/record/record.go b/record/record.go index df4671c..c0c62f0 100644 --- a/record/record.go +++ b/record/record.go @@ -49,6 +49,11 @@ func Collect[K comparable, V, R any](f func(K, V) R) func(map[K]V) []R { return G.Collect[map[K]V, []R](f) } +// CollectOrd applies a collector function to the key value pairs in a map and returns the result as an array +func CollectOrd[V, R any, K comparable](o ord.Ord[K]) func(func(K, V) R) func(map[K]V) []R { + return G.CollectOrd[map[K]V, []R](o) +} + func Reduce[K comparable, V, R any](f func(R, V) R, initial R) func(map[K]V) R { return G.Reduce[map[K]V](f, initial) }