Skip to content

Commit ba696b9

Browse files
authored
Subcontext: add unsafeModify. Open SubcontextImpl class. Rename SubcontextImpl .empty->.initial (#2225)
* Subcontext: add `modify` and `flatAp` methods open SubcontextImpl class rename SubcontextImpl `.empty`->`.initial` * Remove `flatAp`. Rename `modify`->`unsafeModify`. Add documentation as to why it's unsafe. * wip
1 parent a8cfd4b commit ba696b9

File tree

4 files changed

+82
-13
lines changed

4 files changed

+82
-13
lines changed

distage/distage-core-api/src/main/scala/izumi/distage/Subcontext.scala

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package izumi.distage
33
import izumi.distage.model.definition.Identifier
44
import izumi.functional.lifecycle.Lifecycle
55
import izumi.distage.model.plan.Plan
6+
import izumi.distage.model.providers.Functoid
67
import izumi.functional.quasi.QuasiIO
78
import izumi.fundamentals.platform.functional.Identity
89
import izumi.fundamentals.platform.language.CodePositionMaterializer
@@ -19,12 +20,63 @@ trait Subcontext[A] {
1920
* Use `produce` if you need to extend the lifetime of the Subcontext's resources.
2021
*/
2122
def produceRun[F[_]: QuasiIO: TagK, B](f: A => F[B]): F[B]
22-
final def produceRun[B](f: A => B): B = produceRun[Identity, B](f)
23+
24+
final def produceRunSimple[B](f: A => B): B = produceRun[Identity, B](f)
2325

2426
def provide[T: Tag](value: T)(implicit pos: CodePositionMaterializer): Subcontext[A]
2527
def provide[T: Tag](name: Identifier)(value: T)(implicit pos: CodePositionMaterializer): Subcontext[A]
2628

2729
def plan: Plan
2830

29-
def map[B: Tag](f: A => B): Subcontext[B]
31+
final def map[B: Tag](f: A => B): Subcontext[B] = unsafeModify(_.map(f))
32+
33+
/**
34+
* Unsafely substitute the Functoid that extracts the root component.
35+
*
36+
* Note, because the `plan` has been calculated ahead of time with `A`
37+
* as the root, it's not possible to request additional components
38+
* via Functoid that weren't already in the graph as dependencies of `A`
39+
* – everything that `A` doesn't depend is not in the plan.
40+
*
41+
* Example:
42+
*
43+
* {{{
44+
* val submodule = new ModuleDef {
45+
* make[Int].fromValue(3)
46+
* make[Int].named("five").from((_: Int) + 2)
47+
* make[String].fromValue("x")
48+
* }
49+
*
50+
* makeSubcontext[Int].named("five").withSubmodule(submodule)
51+
*
52+
* ...
53+
*
54+
* (subcontext: Subcontext[Int])
55+
* .unsafeModify(_ => Functoid.identity[String])
56+
* .produceRun(println(_))
57+
* // error: `String` is not available
58+
* }}}
59+
*
60+
* A binding for `String` is defined in the submodule, but is not referenced by `Int @Id("five")` binding.
61+
* Therefore it was removed by garbage collection and cannot be extracted with this Subcontext. You'd have to create
62+
* another `Subcontext[String]` with the same submodule to create `String`:
63+
*
64+
* {{{
65+
* makeSubcontext[String](submodule)
66+
*
67+
* ...
68+
*
69+
* (subcontext: Subcontext[String])
70+
* .produceRun(println(_))
71+
* // x
72+
* }}}
73+
*
74+
* If you DO need dynamic replanning, you'll need to use
75+
* [[https://izumi.7mind.io/distage/advanced-features.html#depending-on-locator nested injection]]
76+
* directly.
77+
*/
78+
def unsafeModify[B](f: Functoid[A] => Functoid[B]): Subcontext[B]
79+
80+
@deprecated("Renamed to produceRunSimple", "1.2.17")
81+
final def produceRun[B](f: A => B): B = produceRunSimple[B](f)
3082
}

distage/distage-core/src/main/scala/izumi/distage/SubcontextImpl.scala

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import izumi.functional.quasi.QuasiIO
1313
import izumi.fundamentals.platform.language.CodePositionMaterializer
1414
import izumi.reflect.{Tag, TagK}
1515

16-
final class SubcontextImpl[A] private (
17-
externalKeys: Set[DIKey],
18-
parent: LocatorRef,
16+
open class SubcontextImpl[A](
17+
val externalKeys: Set[DIKey],
18+
val parent: LocatorRef,
1919
val plan: Plan,
20-
functoid: Functoid[A],
21-
providedExternals: Map[DIKey, AnyRef],
22-
selfKey: DIKey,
20+
val functoid: Functoid[A],
21+
val providedExternals: Map[DIKey, AnyRef],
22+
val selfKey: DIKey,
2323
) extends Subcontext[A] {
2424

2525
def provide[T: Tag](value: T)(implicit pos: CodePositionMaterializer): Subcontext[A] = {
@@ -50,8 +50,8 @@ final class SubcontextImpl[A] private (
5050
produce().use(f)
5151
}
5252

53-
override def map[B: Tag](f: A => B): Subcontext[B] = {
54-
new SubcontextImpl(externalKeys, parent, plan, functoid.map[B](f), providedExternals, selfKey)
53+
override def unsafeModify[B](f: Functoid[A] => Functoid[B]): Subcontext[B] = {
54+
new SubcontextImpl(externalKeys, parent, plan, f(functoid), providedExternals, selfKey)
5555
}
5656

5757
private def doAdd(value: AnyRef, pos: CodePositionMaterializer, key: DIKey): SubcontextImpl[A] = {
@@ -72,6 +72,12 @@ final class SubcontextImpl[A] private (
7272
}
7373

7474
object SubcontextImpl {
75-
def empty[A](externalKeys: Set[DIKey], locatorRef: LocatorRef, subplan: Plan, impl: Functoid[A], selfKey: DIKey) =
76-
new SubcontextImpl[A](externalKeys, locatorRef, subplan, impl, Map.empty, selfKey)
75+
def initial[A](externalKeys: Set[DIKey], parent: LocatorRef, subplan: Plan, functoid: Functoid[A], selfKey: DIKey): SubcontextImpl[A] = {
76+
new SubcontextImpl[A](externalKeys, parent, subplan, functoid, Map.empty, selfKey)
77+
}
78+
79+
@deprecated("Renamed to initial", "1.2.17")
80+
def empty[A](externalKeys: Set[DIKey], locatorRef: LocatorRef, subplan: Plan, impl: Functoid[A], selfKey: DIKey): SubcontextImpl[A] = {
81+
initial(externalKeys, locatorRef, subplan, impl, selfKey)
82+
}
7783
}

distage/distage-core/src/main/scala/izumi/distage/provisioning/strategies/SubcontextStrategyDefaultImpl.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class SubcontextStrategyDefaultImpl extends SubcontextStrategy {
2323
val locatorRef = value.asInstanceOf[LocatorRef]
2424
val provider = op.wiring.provider
2525
val subplan = op.wiring.subplan
26-
val ctx = SubcontextImpl.empty[Any](op.wiring.externalKeys, locatorRef, subplan, Functoid(provider), op.target)
26+
val ctx = SubcontextImpl.initial[Any](op.wiring.externalKeys, locatorRef, subplan, Functoid(provider), op.target)
2727
F.pure(Right(Seq(NewObjectOp.UseInstance(op.target, ctx))))
2828

2929
case None =>

doc/microsite/src/main/tut/distage/basics.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,17 @@ Injector()
13971397

13981398
## Subcontexts
13991399

1400+
Subcontext seems to do most of this - it inherits from global scope by default. You can use include inside subcontext's module to spread out definitions into multiple modules. It is limited to a target dependency, but the target dependency itself is not limited - you can make a tuple or a case class binding to aggregate multiple components in a tuple or even add LocatorRef to extract arbitrary components (but they would have to be dependencies of other components in the tuple).
1401+
1402+
It wouldn't make a lot of sense to make Subcontexts fully unrestricted wrt the target dependency - because Subcontexts are actually an optimization of nested injection pattern, where the target dependency is known ahead of time. This allows the subcontext to be pre-planned in advance, so when you create an instance of the subgraph there is no injector overhead - the constructors are just called with local dependencies according to the pre-calculated plan. Since values of local dependencies can't influence the shape of the graph there is no reason to recalculate it.
1403+
1404+
Now, if you actually need to be able to make different subgraphs out of a module at runtime, you can manually use nested injection - that way you have full flexibility with regards to everything, you can even do another classpath scan for Plugins and construct the module from that at runtime.
1405+
1406+
However, for efficiency, you'd probably want to use pre-calculated subcontexts, because people rarely make dynamically extensible applications especially in Scala. It would be easier to just make multiple subcontexts for each different component - they can even use the same module, it doesn't matter, only real dependencies of the target dependency will be created, modules can safely contain unused bindings:
1407+
1408+
1409+
Note also that Subcontexts are generalized Factories. A subcontext with just one binding is exactly the same as a Factory.
1410+
14001411
Sometimes multiple components depend on the same piece of data that appears locally, after all the components were already wired.
14011412
This data may need to be passed around repeatedly, possibly across the entire application. To do this, we may have to add an argument
14021413
to most methods of an application, or have to use a Reader monad everywhere.

0 commit comments

Comments
 (0)