Skip to content

Commit b067b71

Browse files
committed
added note abot taagess-final dependency injection
1 parent 4e07fa7 commit b067b71

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
---
2+
title: small type-driven dependency injection in effect systems.
3+
---
4+
5+
At first, detective history with Reddit moderation: During the previous post form this series, it was a comment about the effect systems. I have give two answers: one with a technical overview a second one with the note that it is possible to use static AppContextProvider. Few days later I accidentally sow this discussion on the other computer, where I was not logged to reddit, and discover that my first reply was deleted by moderator. Interesting, that I don’t see this if I logged by itself. Quite strange, I guess this is automatic moderation based on some pattern.
6+
7+
Ok. Let’s adjust our type-based injection framework to the effect systems. This text is a result of a joint work with Ivan Kyrylov during GSoC-2024. The main work was not about dependency injection but abstract representations of effect, but static dependency injection was a starting point for Ivan's journey. Our first attempt was based on another approach, then here (we trying to automatically assemble typemap of needed injections, which as state of scala-3.x is impossible, because we can’t return context function from macros), but during this stage we receive some understanding, what should work.
8+
9+
At first, what make dependency injection different in the effect system environment?
10+
- Types of dependencies are encoded into the type of enclosing monad.
11+
- Retrieving of dependencies can be asynchronous:
12+
13+
I.e., typical usage of dependency injection in the effect environment looks like:
14+
15+
## Tagless-Final style
16+
17+
```Scala
18+
def newSubscriber1[F[_]:InAppContext[(UserDatabase[F],EmailService)]:Effect](user: User):F[User] = {
19+
for{
20+
db <- InAppContext.get[F, UserDatabase[F]]
21+
u <- db.insert(user)
22+
emailService <- AppContextEffect.get[F, EmailService]
23+
_ <- emailService.sendEmail(u, "You have been subscribed")
24+
} yield u
25+
```
26+
27+
Here, we assume tagless-final style and dependencies `(UserDatabase[F],EmailService)` are listed as properties of `F[_]` I.e., exists
28+
`InAppContext[(UserDatabase[F],EmailService)][F]` from which we can retrieve monadized references during computations.
29+
30+
We can define InAppContex as a reference to the list of providers:
31+
32+
```Scala
33+
type InAppContext[D <: NonEmptyTuple] = [F[_]] =>> AppContextAsyncProviders[F,D]
34+
```
35+
36+
Where AppContextAsyncProviders is constructed in the same way as `AppContextProvider` for the core case.
37+
38+
Before diving into details, let’s speak about the second difference: monadic (or asynchronous) retrieving of dependencies:
39+
40+
```
41+
trait AppContextAsyncProvider[F[_],T] {
42+
43+
def get: F[T]
44+
45+
}
46+
```
47+
Here, the async provider returns the value wrapped in the monad. This wrapping makes sense when a monad provides additional logic necessary for the dependency lifecycle, such as acquiring or releasing a resource. Note that this form is not strictly needed because we can achieve the same logic by changing the API form. But let’s follow traditions.
48+
49+
Of course, we can make Async provider from Sync:
50+
51+
```Scala
52+
given fromSync[F[_] : AppContextPure, T](using syncProvider: AppContextProvider[T]): AppContextAsyncProvider[F, T] with
53+
def get: F[T] = summon[Pure[F]].pure(syncProvider.get)
54+
```
55+
56+
But note that for this, we should have defined somewhere `Pure` typeclass (which is absent in the Scala standard library). Also, in theory, `syncProvider.get` can produce side effects, but developers who prefer pure functional style will choose to delay potentially effectful invocation… Yet one issue – `pure` in cats is eager…, so maybe better wording exists… Let’s define our generic typeclass, [AppContextPure](https://github.com/rssh/scala-appcontext/blob/59014c7aecacf81ea3fb6f9415ed603001032248/tagless-final/shared/src/main/scala/com/github/rssh/appcontext/util/AppContextPure.scala#L5) and provide implementation based on dotty-cps-async (which becomes an optional dependency).
57+
If you have an idea for better wording, please write a comment.
58+
59+
Ok, now return to constructing providers.
60+
Let we have a method with signature:
61+
62+
```Scala
63+
def newSubscriber1[F[_]:InAppContext[(UserDatabase[F],EmailService)]:Effect](user: User):F[User] = ...
64+
```
65+
66+
When we call this method from upside of `newSubscriber` scope, we should synthesize `AppContextProviders[F,(UserDatabase[F],EmailService)]` by searching providers for the tuple elements.
67+
When we call this method from inside, we should resolve services if they are in the AppContextProviders tuple. At first glance, we can build macros that build AppContextProviders like in the core (described in the previous post).
68+
But wait, there is one difference: AppContextAsyncProviders are always in the lexical scope inside the `newSubscriber` function. This means that a search for an AsyncProvider can trigger the creation of an implicit instance of `AppContextAsyncProviders`.
69+
70+
Let’s look at the next block of code:
71+
72+
```Scala
73+
trait ConnectionPool {
74+
def get[F[_]:Effect]():F[Connection]
75+
}
76+
77+
trait UserDatabase[F[_]:Effect:InAppContext[ConnectionPool *: EmptyTuple]] {
78+
def insert(user: User):F[User]
79+
}
80+
81+
object UserDatabase {
82+
given [F[_]:Effect:InAppContext[ConnectionPool *: EmptyTuple]]
83+
: AppContextAsyncProvider[F,UserDatabase[F]]
84+
}
85+
86+
def newSubscriber1[F[_]:InAppContext[(UserDatabase[F],EmailService)]:Effect](user: User):F[User] = {
87+
...
88+
}
89+
90+
91+
def main(): Unit = {
92+
given EmailService = new MyLocalEmailService
93+
given ConnectionPool = new MyLocalConnectionPool
94+
val user = User("John", "[email protected]")
95+
val r = newSubscriber1[ToyMonad](user)
96+
val fr = ToyMonad.run(r)
97+
}
98+
99+
100+
```
101+
(assuming minimal [ToyMonad](https://github.com/rssh/scala-appcontext/blob/59014c7aecacf81ea3fb6f9415ed603001032248/tagless-final/shared/src/test/scala/com/github/rssh/toymonad/ToyMonad.scala#L15) )
102+
103+
Here, new subscriber bounds will trigger search for `UserDatabase`(1) which will trigger search for `ConnectionPool`(3) which at first will be searched in the `InAppContext[..][F]` scope which will trigger building of `AppContextProviders[ConnectionPool*:EmptyTuple]`(4) which will be called because `InAppContext[(ConnectionPool *: EmptyTuple])` is a type parameter of enclosing function and then will start searching in enclosing scope (5).
104+
105+
The problem is that if step (4) triggers our macro and the macro produces an error, we will report an error, not be able to continue a search, and never reach step (5).
106+
107+
At the core, we escape this problem by defining the class `AppContextProvidersSearch`. But now we can’t do this.
108+
109+
Let’s think about how we make a macro for implicit search, which will fail the search without producing an error. For this, our macro should also return some result (success or failure), with type determined by our macro, and use evidence to success in the implicit search for value:
110+
111+
```Scala
112+
object AppContextAsyncProviders {
113+
114+
trait TryBuild[F[_], Xs<:NonEmptyTuple]
115+
case class TryBuildSuccess[F[_],Xs<:NonEmptyTuple](providers:AppContextAsyncProviders[F,Xs]) extends TryBuild[F,Xs]
116+
case class TryBuildFailure[F[_],Xs<:NonEmptyTuple](message: String) extends TryBuild[F,Xs]
117+
118+
transparent inline given tryBuild[F[_],Xs <:NonEmptyTuple]: TryBuild[F,Xs] = ${
119+
tryBuildImpl[F,Xs]
120+
}
121+
122+
inline given build[F[_]:CpsMonad, Xs <: NonEmptyTuple, R <: TryBuild[F,Xs]](using inline trb: R, inline ev: R <:< TryBuildSuccess[F,Xs]): AppContextAsyncProviders[F,Xs] = {
123+
trb.providers
124+
}
125+
126+
def tryBuildImpl[F[_]:Type, Xs <: NonEmptyTuple:Type](using Quotes): Expr[TryBuild[F,Xs]] = {
127+
//
128+
}
129+
130+
..
131+
132+
}
133+
134+
135+
```
136+
137+
Full code: [AppContextProviders](https://github.com/rssh/scala-appcontext/blob/main/tagless-final/shared/src/main/scala/com/github/rssh/appcontext/AppContextAsyncProviders.scala).
138+
139+
Now, let’s port the standard example to the monadic case: [see example 3](https://github.com/rssh/scala-appcontext/blob/59014c7aecacf81ea3fb6f9415ed603001032248/tagless-final/shared/src/test/scala/com/github/rssh/appcontext/Example3Test.scala#L12). Next block of code instantiate and pass `UserDatabase` to the `newSubscrber`, under the hood.
140+
141+
```Scala
142+
given EmailService ..
143+
given ConnectionPool = ..
144+
145+
146+
val user = User("John", "[email protected]")
147+
val r = newSubscriber1[ToyMonad](user)
148+
```
149+
150+
Hmm... actually we don't use `AppContextAsyncProvider`.
151+
152+
Let’s make model example close to reality: use real IO and async Connection created in resource:
153+
154+
See [Example 5](https://github.com/rssh/scala-appcontext/blob/main/tagless-final/jvm/src/test/scala/com/github/rssh/appcontexttest/Example5Test.scala)
155+
156+
157+
## Concrete monad style
158+
159+
Yet one popular style is using a concrete monads, for example `IO` instead `F[_]`. In such case we don’t need `InAppContext` and can pass providers as in core case, as context parameters. What providers to use: `AppContextProvider or AppContextAsyncProviders` become a question of taste. You even can use `AppContextProviderModule` with async dependencies.
160+
161+
[Example](https://github.com/rssh/scala-appcontext/blob/main/tagless-final/jvm/src/test/scala/com/github/rssh/appcontexttest/Example7Test.scala)
162+
163+
## Environment effects.
164+
165+
If we open the theme of using type-driven dependency injection in the effect systems, maybe we should say few words about libraries like zio or kyo, which provides own implementation of dependency injection.
166+
All them based on concept, that types needed for computation are encoded in it signature (similar to our tagless-final approach). In theory, our approach cah simplicity interaction points with such libraries (i.e. we can assemble needed computation environmemt from providers).
167+
168+
169+
That’s all for today. Tagless final part are published as subproject in appcontext with name “appcontext-tf”,
170+
(github: https://github.com/rssh/scala-appcontext )
171+
You can try it using `“com.github.rssh” %%% “appcontext-tf” % “0.2.0”` as dependency. (maybe it should be joined with core ?) Will be grateful about problem reports and suggestions for better names.
172+
173+
174+
175+
176+
177+
178+
179+
180+
181+
182+

0 commit comments

Comments
 (0)