-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Copy pathProviders.scala
228 lines (176 loc) · 6.73 KB
/
Providers.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import compiletime.constValue
import compiletime.ops.int.S
// Featherweight dependency injection library, inspired by the use case
// laid out in the ZIO course of RockTheJVM.
/** Some things that are not part of Tuple yet, but that would be nice to have. */
object TupleUtils:
/** The index of the first element type of the tuple `Xs` that is a subtype of `X` */
type IndexOf[Xs <: Tuple, X] <: Int = Xs match
case X *: _ => 0
case _ *: ys => S[IndexOf[ys, X]]
/** A trait describing a selection from a tuple `Xs` returning an element of type `X` */
trait Select[Xs <: Tuple, X]:
def apply(xs: Xs): X
/** A given implementing `Select` to return the first element of tuple `Xs`
* that has a static type matching `X`.
*/
given [Xs <: NonEmptyTuple, X] => (idx: ValueOf[IndexOf[Xs, X]]) => Select[Xs, X]:
def apply(xs: Xs) = xs.apply(idx.value).asInstanceOf[X]
/** A featherweight library for dependency injection */
object Providers:
import TupleUtils.*
/** A provider is a zero-cost wrapper around a type that is intended
* to be passed implicitly
*/
opaque type Provider[T] = T
def provide[X](x: X): Provider[X] = x
def provided[X](using p: Provider[X]): X = p
/** Project a provider to one of its element types */
given [Xs <: Tuple, X] => (ps: Provider[Xs], select: Select[Xs, X]) => Provider[X] =
select(ps)
/** Form a compound provider wrapping a tuple */
given [X, Xs <: Tuple] => (p: Provider[X], ps: Provider[Xs]) => Provider[X *: Xs] =
p *: ps
given Provider[EmptyTuple] = EmptyTuple
end Providers
@main def Test =
import TupleUtils.*
type P = (Int, String, List[Int])
val x: P = (11, "hi", List(1, 2, 3))
val selectInt = summon[Select[P, Int]]
println(selectInt(x))
val selectString = summon[Select[P, String]]
println(selectString(x))
val selectList = summon[Select[P, List[Int]]]
println(selectList(x))
val selectObject = summon[Select[P, Object]]
println(selectObject(x)) // prints "hi"
println(s"\nDirect:")
Explicit().test()
println(s"\nInjected")
Injected().test()
println(s"\nInjected2")
Injected2().test()
/** Demonstrator for explicit dependency construction */
class Explicit:
case class User(name: String, email: String)
class UserSubscription(emailService: EmailService, db: UserDatabase):
def subscribe(user: User) =
emailService.email(user)
db.insert(user)
class EmailService:
def email(user: User) =
println(s"You've just been subscribed to RockTheJVM. Welcome, ${user.name}")
class UserDatabase(pool: ConnectionPool):
def insert(user: User) =
val conn = pool.get()
conn.runQuery(s"insert into subscribers(name, email) values ${user.name} ${user.email}")
class ConnectionPool(n: Int):
def get(): Connection =
println(s"Acquired connection")
Connection()
class Connection():
def runQuery(query: String): Unit =
println(s"Executing query: $query")
def test() =
val subscriptionService =
UserSubscription(
EmailService(),
UserDatabase(
ConnectionPool(10)
)
)
def subscribe(user: User) =
val sub = subscriptionService
sub.subscribe(user)
subscribe(User("Daniel", "[email protected]"))
subscribe(User("Martin", "[email protected]"))
end Explicit
/** The same application as `Explicit` but using dependency injection */
class Injected:
import Providers.*
case class User(name: String, email: String)
class UserSubscription(using Provider[(EmailService, UserDatabase)]):
def subscribe(user: User) =
provided[EmailService].email(user)
provided[UserDatabase].insert(user)
class EmailService:
def email(user: User) =
println(s"You've just been subscribed to RockTheJVM. Welcome, ${user.name}")
class UserDatabase(using Provider[ConnectionPool]):
def insert(user: User) =
val conn = provided[ConnectionPool].get()
conn.runQuery(s"insert into subscribers(name, email) values ${user.name} ${user.email}")
class ConnectionPool(n: Int):
def get(): Connection =
println(s"Acquired connection")
Connection()
class Connection():
def runQuery(query: String): Unit =
println(s"Executing query: $query")
def test() =
given Provider[EmailService] = provide(EmailService())
given Provider[ConnectionPool] = provide(ConnectionPool(10))
given Provider[UserDatabase] = provide(UserDatabase())
given Provider[UserSubscription] = provide(UserSubscription())
def subscribe(user: User)(using Provider[UserSubscription]) =
val sub = provided[UserSubscription]
sub.subscribe(user)
subscribe(User("Daniel", "[email protected]"))
subscribe(User("Martin", "[email protected]"))
end test
// explicit version, not used here
object explicit:
val subscriptionService =
UserSubscription(
using provide(
EmailService(),
UserDatabase(
using provide(
ConnectionPool(10)
)
)
)
)
given Provider[UserSubscription] = provide(subscriptionService)
end explicit
end Injected
/** Injected with builders in companion objects */
class Injected2:
import Providers.*
case class User(name: String, email: String)
class UserSubscription(emailService: EmailService, db: UserDatabase):
def subscribe(user: User) =
emailService.email(user)
db.insert(user)
object UserSubscription:
def apply()(using Provider[(EmailService, UserDatabase)]): UserSubscription =
new UserSubscription(provided[EmailService], provided[UserDatabase])
class EmailService:
def email(user: User) =
println(s"You've just been subscribed to RockTheJVM. Welcome, ${user.name}")
class UserDatabase(pool: ConnectionPool):
def insert(user: User) =
pool.get().runQuery(s"insert into subscribers(name, email) values ${user.name} ${user.email}")
object UserDatabase:
def apply()(using Provider[(ConnectionPool)]): UserDatabase =
new UserDatabase(provided[ConnectionPool])
class ConnectionPool(n: Int):
def get(): Connection =
println(s"Acquired connection")
Connection()
class Connection():
def runQuery(query: String): Unit =
println(s"Executing query: $query")
def test() =
given Provider[EmailService] = provide(EmailService())
given Provider[ConnectionPool] = provide(ConnectionPool(10))
given Provider[UserDatabase] = provide(UserDatabase())
given Provider[UserSubscription] = provide(UserSubscription())
def subscribe(user: User)(using Provider[UserSubscription]) =
val sub = UserSubscription()
sub.subscribe(user)
subscribe(User("Daniel", "[email protected]"))
subscribe(User("Martin", "[email protected]"))
end test
end Injected2