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

fix: add initial DI implementation

Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
Carsten Leue
2023-11-09 19:44:11 +01:00
parent 37430c0698
commit 16f708d69f
13 changed files with 656 additions and 126 deletions

View File

@@ -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

View File

@@ -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),
)
}

View File

@@ -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
}

33
di/injector.go Normal file
View File

@@ -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]()),
)
}

View File

@@ -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),
),
)
}

View File

@@ -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)
}

View File

@@ -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]()),
}
}

62
di/utils.go Normal file
View File

@@ -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],
))),
)
}

64
di/utils_test.go Normal file
View File

@@ -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")))))
}

View File

@@ -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]),
)
}

View File

@@ -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])
}

View File

@@ -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)

View File

@@ -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)
}