Skip to content

Commit 02c3738

Browse files
authored
Merge pull request #171 from 47deg/ds-identity
Add F[_] type parameter to DataSource
2 parents 06556e7 + 61a28be commit 02c3738

15 files changed

Lines changed: 792 additions & 619 deletions

File tree

README.md

Lines changed: 76 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,11 @@ Data Sources take two type parameters:
5353
import cats.data.NonEmptyList
5454
import cats.effect.ConcurrentEffect
5555

56-
trait DataSource[Identity, Result]{
57-
def name: String
58-
def fetch[F[_] : ConcurrentEffect](id: Identity): F[Option[Result]]
59-
def batch[F[_] : ConcurrentEffect](ids: NonEmptyList[Identity]): F[Map[Identity, Result]]
56+
trait DataSource[F[_], Identity, Result]{
57+
def data: Data[Identity, Result]
58+
def CF: ConcurrentEffect[F]
59+
def fetch(id: Identity): F[Option[Result]]
60+
def batch(ids: NonEmptyList[Identity]): F[Map[Identity, Result]]
6061
}
6162
```
6263

@@ -65,31 +66,42 @@ Returning `ConcurrentEffect` instances from the fetch methods allows us to speci
6566
We'll implement a dummy data source that can convert integers to strings. For convenience, we define a `fetchString` function that lifts identities (`Int` in our dummy data source) to a `Fetch`.
6667

6768
```scala
69+
import cats._
6870
import cats.data.NonEmptyList
6971
import cats.effect._
7072
import cats.instances.list._
73+
import cats.implicits._
7174
import cats.syntax.all._
7275

7376
import fetch._
7477

75-
object ToStringSource extends DataSource[Int, String]{
76-
override def name = "ToString"
78+
def latency[F[_] : Effect](milis: Long): F[Unit] =
79+
Effect[F].delay(Thread.sleep(milis))
7780

78-
override def fetch[F[_] : ConcurrentEffect](id: Int): F[Option[String]] = {
79-
Sync[F].delay(println(s"--> [${Thread.currentThread.getId}] One ToString $id")) >>
80-
Sync[F].delay(println(s"<-- [${Thread.currentThread.getId}] One ToString $id")) >>
81-
Sync[F].pure(Option(id.toString))
82-
}
81+
object ToString extends Data[Int, String] {
82+
def name = "To String"
83+
84+
def source[F[_] : ConcurrentEffect]: DataSource[F, Int, String] = new DataSource[F, Int, String]{
85+
override def data = ToString
86+
87+
override def CF = ConcurrentEffect[F]
8388

84-
override def batch[F[_] : ConcurrentEffect](ids: NonEmptyList[Int]): F[Map[Int, String]] = {
85-
Sync[F].delay(println(s"--> [${Thread.currentThread.getId}] Batch ToString $ids")) >>
86-
Sync[F].delay(println(s"<-- [${Thread.currentThread.getId}] Batch ToString $ids")) >>
87-
Sync[F].pure(ids.toList.map(i => (i, i.toString)).toMap)
89+
override def fetch(id: Int): F[Option[String]] = for {
90+
_ <- CF.delay(println(s"--> [${Thread.currentThread.getId}] One ToString $id"))
91+
_ <- latency(100)
92+
_ <- CF.delay(println(s"<-- [${Thread.currentThread.getId}] One ToString $id"))
93+
} yield Option(id.toString)
94+
95+
override def batch(ids: NonEmptyList[Int]): F[Map[Int, String]] = for {
96+
_ <- CF.delay(println(s"--> [${Thread.currentThread.getId}] Batch ToString $ids"))
97+
_ <- latency(100)
98+
_ <- CF.delay(println(s"<-- [${Thread.currentThread.getId}] Batch ToString $ids"))
99+
} yield ids.toList.map(i => (i, i.toString)).toMap
88100
}
89101
}
90102

91103
def fetchString[F[_] : ConcurrentEffect](n: Int): Fetch[F, String] =
92-
Fetch(n, ToStringSource)
104+
Fetch(n, ToString.source)
93105
```
94106

95107
## Creating a runtime
@@ -125,8 +137,8 @@ import scala.concurrent.duration._
125137
// import scala.concurrent.duration._
126138

127139
Fetch.run[IO](fetchOne).unsafeRunTimed(5.seconds)
128-
// --> [179] One ToString 1
129-
// <-- [179] One ToString 1
140+
// --> [109] One ToString 1
141+
// <-- [109] One ToString 1
130142
// res0: Option[String] = Some(1)
131143
```
132144

@@ -145,26 +157,32 @@ When executing the above fetch, note how the three identities get batched and th
145157

146158
```scala
147159
Fetch.run[IO](fetchThree).unsafeRunTimed(5.seconds)
148-
// --> [179] Batch ToString NonEmptyList(1, 2, 3)
149-
// <-- [179] Batch ToString NonEmptyList(1, 2, 3)
160+
// --> [109] Batch ToString NonEmptyList(1, 2, 3)
161+
// <-- [109] Batch ToString NonEmptyList(1, 2, 3)
150162
// res1: Option[(String, String, String)] = Some((1,2,3))
151163
```
152164

153165
Note that the `DataSource#batch` method is not mandatory, it will be implemented in terms of `DataSource#fetch` if you don't provide an implementation.
154166

155167
```scala
156-
object UnbatchedToStringSource extends DataSource[Int, String]{
157-
override def name = "UnbatchedToString"
168+
object UnbatchedToString extends Data[Int, String] {
169+
def name = "Unbatched to string"
170+
171+
def source[F[_] : ConcurrentEffect] = new DataSource[F, Int, String] {
172+
override def data = UnbatchedToString
173+
174+
override def CF = ConcurrentEffect[F]
158175

159-
override def fetch[F[_] : ConcurrentEffect](id: Int): F[Option[String]] = {
160-
Sync[F].delay(println(s"--> [${Thread.currentThread.getId}] One UnbatchedToString $id")) >>
161-
Sync[F].delay(println(s"<-- [${Thread.currentThread.getId}] One UnbatchedToString $id")) >>
162-
Sync[F].pure(Option(id.toString))
176+
override def fetch(id: Int): F[Option[String]] =
177+
CF.delay(println(s"--> [${Thread.currentThread.getId}] One UnbatchedToString $id")) >>
178+
latency(100) >>
179+
CF.delay(println(s"<-- [${Thread.currentThread.getId}] One UnbatchedToString $id")) >>
180+
CF.pure(Option(id.toString))
163181
}
164182
}
165183

166184
def unbatchedString[F[_] : ConcurrentEffect](n: Int): Fetch[F, String] =
167-
Fetch(n, UnbatchedToStringSource)
185+
Fetch(n, UnbatchedToString.source)
168186
```
169187

170188
Let's create a tuple of unbatched string requests.
@@ -178,12 +196,12 @@ When executing the above fetch, note how the three identities get requested in p
178196

179197
```scala
180198
Fetch.run[IO](fetchUnbatchedThree).unsafeRunTimed(5.seconds)
181-
// --> [179] One UnbatchedToString 1
182-
// --> [181] One UnbatchedToString 3
183-
// <-- [181] One UnbatchedToString 3
184-
// --> [182] One UnbatchedToString 2
185-
// <-- [182] One UnbatchedToString 2
186-
// <-- [179] One UnbatchedToString 1
199+
// --> [109] One UnbatchedToString 1
200+
// --> [111] One UnbatchedToString 3
201+
// --> [112] One UnbatchedToString 2
202+
// <-- [109] One UnbatchedToString 1
203+
// <-- [111] One UnbatchedToString 3
204+
// <-- [112] One UnbatchedToString 2
187205
// res2: Option[(String, String, String)] = Some((1,2,3))
188206
```
189207

@@ -192,23 +210,30 @@ Fetch.run[IO](fetchUnbatchedThree).unsafeRunTimed(5.seconds)
192210
If we combine two independent fetches from different data sources, the fetches can be run in parallel. First, let's add a data source that fetches a string's size.
193211

194212
```scala
195-
object LengthSource extends DataSource[String, Int]{
196-
override def name = "Length"
213+
object Length extends Data[String, Int] {
214+
def name = "Length"
197215

198-
override def fetch[F[_] : ConcurrentEffect](id: String): F[Option[Int]] = {
199-
Sync[F].delay(println(s"--> [${Thread.currentThread.getId}] One Length $id")) >>
200-
Sync[F].delay(println(s"<-- [${Thread.currentThread.getId}] One Length $id")) >>
201-
Sync[F].pure(Option(id.size))
202-
}
203-
override def batch[F[_] : ConcurrentEffect](ids: NonEmptyList[String]): F[Map[String, Int]] = {
204-
Sync[F].delay(println(s"--> [${Thread.currentThread.getId}] Batch Length $ids")) >>
205-
Sync[F].delay(println(s"<-- [${Thread.currentThread.getId}] Batch Length $ids")) >>
206-
Sync[F].pure(ids.toList.map(i => (i, i.size)).toMap)
216+
def source[F[_] : ConcurrentEffect] = new DataSource[F, String, Int] {
217+
override def data = Length
218+
219+
override def CF = ConcurrentEffect[F]
220+
221+
override def fetch(id: String): F[Option[Int]] = for {
222+
_ <- CF.delay(println(s"--> [${Thread.currentThread.getId}] One Length $id"))
223+
_ <- latency(100)
224+
_ <- CF.delay(println(s"<-- [${Thread.currentThread.getId}] One Length $id"))
225+
} yield Option(id.size)
226+
227+
override def batch(ids: NonEmptyList[String]): F[Map[String, Int]] = for {
228+
_ <- CF.delay(println(s"--> [${Thread.currentThread.getId}] Batch Length $ids"))
229+
_ <- latency(100)
230+
_ <- CF.delay(println(s"<-- [${Thread.currentThread.getId}] Batch Length $ids"))
231+
} yield ids.toList.map(i => (i, i.size)).toMap
207232
}
208233
}
209234

210235
def fetchLength[F[_] : ConcurrentEffect](s: String): Fetch[F, Int] =
211-
Fetch(s, LengthSource)
236+
Fetch(s, Length.source)
212237
```
213238

214239
And now we can easily receive data from the two sources in a single fetch.
@@ -222,10 +247,10 @@ Note how the two independent data fetches run in parallel, minimizing the latenc
222247

223248
```scala
224249
Fetch.run[IO](fetchMulti).unsafeRunTimed(5.seconds)
225-
// --> [181] One ToString 1
226-
// <-- [181] One ToString 1
227-
// --> [180] One Length one
228-
// <-- [180] One Length one
250+
// --> [109] One ToString 1
251+
// --> [110] One Length one
252+
// <-- [109] One ToString 1
253+
// <-- [110] One Length one
229254
// res3: Option[(String, Int)] = Some((1,3))
230255
```
231256

@@ -246,8 +271,8 @@ While running it, notice that the data source is only queried once. The next tim
246271

247272
```scala
248273
Fetch.run[IO](fetchTwice).unsafeRunTimed(5.seconds)
249-
// --> [182] One ToString 1
250-
// <-- [182] One ToString 1
274+
// --> [111] One ToString 1
275+
// <-- [111] One ToString 1
251276
// res4: Option[(String, String)] = Some((1,1))
252277
```
253278

@@ -258,7 +283,6 @@ Fetch.run[IO](fetchTwice).unsafeRunTimed(5.seconds)
258283

259284
For more in-depth information take a look at our [documentation](http://47deg.github.io/fetch/docs.html).
260285

261-
262286
## Fetch in the wild
263287

264288
If you wish to add your library here please consider a PR to include it in the list below.

debug/shared/src/main/scala/debug.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,21 @@ object debug {
8181
}
8282

8383
def showRequest(r: Request): Document = r.request match {
84-
case FetchOne(id, ds) =>
85-
Document.text(s"[Fetch one] From `${ds.name}` with id ${id}") :: showDuration(r.duration)
86-
case Batch(ids, ds) =>
87-
Document.text(s"[Batch] From `${ds.name}` with ids ${ids.toList}") :: showDuration(r.duration)
84+
case FetchOne(id, d) =>
85+
Document.text(s"[Fetch one] From `${d.name}` with id ${id}") :: showDuration(r.duration)
86+
case Batch(ids, d) =>
87+
Document.text(s"[Batch] From `${d.name}` with ids ${ids.toList}") :: showDuration(r.duration)
8888
}
8989

90-
def showMissing(ds: DataSource[_, _], ids: List[_]): Document =
91-
Document.text(s"`${ds.name}` missing identities ${ids}")
90+
def showMissing(d: Data[_, _], ids: List[_]): Document =
91+
Document.text(s"`${d.name}` missing identities ${ids}")
9292

9393
def showRoundCount(err: FetchException): Document =
9494
Document.text(s", fetch interrupted after ${err.environment.rounds.size} rounds")
9595

9696
def showException(err: FetchException): Document = err match {
9797
case MissingIdentity(id, q, env) =>
98-
Document.text(s"[ERROR] Identity with id `${id}` for data source `${q.dataSource.name}` not found") :: showRoundCount(err)
98+
Document.text(s"[ERROR] Identity with id `${id}` for data source `${q.data.name}` not found") :: showRoundCount(err)
9999
case UnhandledException(exc, env) =>
100100
Document
101101
.text(s"[ERROR] Unhandled `${exc.getClass.getName}`: '${exc.getMessage}'") :: showRoundCount(err)

0 commit comments

Comments
 (0)