-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support asynchronous data sources #54
Changes from all commits
02cde66
8b43e76
69081f8
e837ad8
2dfa3b4
6a14cc4
c22d13c
e2b1647
5e48ae0
985f0c3
adbf420
e8d5e87
7f9f86a
02cfb03
620c1e8
b775ea9
c7f0c3a
1a4d6ff
6bde038
f5b83ab
a58c251
3aef47b
be095c0
0c8ca32
ba68bce
1b3d118
d2f99f2
0d92468
158a12a
2234f68
a34ae16
ad842d6
d22eefe
8088021
b229d8f
8525f67
20172b0
00e0a7d
daa9d59
d80da1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,20 +12,20 @@ A library for Simple & Efficient data access in Scala and Scala.js | |
|
||
Add the following dependency to your project's build file. | ||
|
||
For Scala 2.11.x: | ||
|
||
```scala | ||
"com.fortysevendeg" %% "fetch" %% "0.2.0" | ||
"com.fortysevendeg" %% "fetch" % "0.2.0" | ||
``` | ||
|
||
Or, if using Scala.js: | ||
Or, if using Scala.js (0.6.x): | ||
|
||
```scala | ||
"com.fortysevendeg" %%% "fetch" %% "0.2.0" | ||
"com.fortysevendeg" %%% "fetch" % "0.2.0" | ||
``` | ||
|
||
Fetch is available for the following Scala and Scala.js versions: | ||
|
||
- Scala 2.11.x | ||
- Scala.js 0.6.x | ||
|
||
|
||
## Remote data | ||
|
||
|
@@ -34,42 +34,41 @@ has a latency cost, such as databases or web services. | |
|
||
## Define your data sources | ||
|
||
For telling `Fetch` how to get the data you want, you must implement the `DataSource` typeclass. Data sources have a `fetch` method that | ||
defines how to fetch such a piece of data. | ||
To tell `Fetch` how to get the data you want, you must implement the `DataSource` typeclass. Data sources have `fetchOne` and `fetchMany` methods that define how to fetch such a piece of data. | ||
|
||
Data Sources take two type parameters: | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would add something into the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good suggestion, i'll make sure i go into more detail about what I mean by |
||
<ol> | ||
<li><code>Identity</code> is a type that has enough information to fetch the data</li> | ||
<li><code>Result</code> is the type of data we want to fetch</li> | ||
<li><code>Identity</code> is a type that has enough information to fetch the data. For a users data source, this would be a user's unique ID.</li> | ||
<li><code>Result</code> is the type of data we want to fetch. For a users data source, this would the `User` type.</li> | ||
</ol> | ||
|
||
```scala | ||
import cats.data.NonEmptyList | ||
|
||
trait DataSource[Identity, Result]{ | ||
def fetchOne(id: Identity): Eval[Option[Result]] | ||
def fetchMany(ids: NonEmptyList[Identity]): Eval[Map[Identity, Result]] | ||
def fetchOne(id: Identity): Query[Option[Result]] | ||
def fetchMany(ids: NonEmptyList[Identity]): Query[Map[Identity, Result]] | ||
} | ||
``` | ||
|
||
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`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it worth showing a non-convenient version of this? i.e. how would it look without the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is just a matter of using |
||
|
||
```scala | ||
import cats.Eval | ||
import cats.data.NonEmptyList | ||
import cats.std.list._ | ||
|
||
import fetch._ | ||
|
||
implicit object ToStringSource extends DataSource[Int, String]{ | ||
override def fetchOne(id: Int): Eval[Option[String]] = { | ||
Eval.later({ | ||
println(s"ToStringSource $id") | ||
override def fetchOne(id: Int): Query[Option[String]] = { | ||
Query.sync({ | ||
println(s"[${Thread.currentThread.getId}] One ToString $id") | ||
Option(id.toString) | ||
}) | ||
} | ||
override def fetchMany(ids: NonEmptyList[Int]): Eval[Map[Int, String]] = { | ||
Eval.later({ | ||
println(s"ToStringSource $ids") | ||
override def fetchMany(ids: NonEmptyList[Int]): Query[Map[Int, String]] = { | ||
Query.sync({ | ||
println(s"[${Thread.currentThread.getId}] Many ToString $ids") | ||
ids.unwrap.map(i => (i, i.toString)).toMap | ||
}) | ||
} | ||
|
@@ -83,21 +82,26 @@ def fetchString(n: Int): Fetch[String] = Fetch(n) // or, more explicitly: Fetch( | |
Now that we can convert `Int` values to `Fetch[String]`, let's try creating a fetch. | ||
|
||
```scala | ||
import fetch.implicits._ | ||
import fetch.syntax._ | ||
|
||
val fetchOne: Fetch[String] = fetchString(1) | ||
``` | ||
|
||
Now that we have created a fetch, we can run it to a target monad. Note that the target monad (`Eval` in our example) needs to implement `MonadError[M, Throwable]`, we provide an instance for `Eval` in `fetch.implicits._`, that's why we imported it. | ||
We'll run our fetches to the ambien `Id` monad in our examples. Note that in real-life scenarios you'll want to run a fetch to a concurrency monad such as `Future` or `Task`, synchronous execution of a fetch is only supported in Scala and not Scala.js and is meant for experimentation purposes. | ||
|
||
```scala | ||
val result: String = fetchOne.runA[Eval].value | ||
// ToStringSource 1 | ||
// result: String = 1 | ||
import cats.Id | ||
import fetch.unsafe.implicits._ | ||
import fetch.syntax._ | ||
``` | ||
|
||
As you can see in the previous example, the `ToStringSource` is queried once to get the value of 1. | ||
Let's run it and wait for the fetch to complete: | ||
|
||
```scala | ||
fetchOne.runA[Id] | ||
// [169] One ToString 1 | ||
// res3: cats.Id[String] = 1 | ||
``` | ||
|
||
## Batching | ||
|
||
|
@@ -112,27 +116,29 @@ val fetchThree: Fetch[(String, String, String)] = (fetchString(1) |@| fetchStrin | |
When executing the above fetch, note how the three identities get batched and the data source is only queried once. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it fair to say that we could also use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since identities are batched |
||
|
||
```scala | ||
val result: (String, String, String) = fetchThree.runA[Eval].value | ||
// ToStringSource OneAnd(1,List(2, 3)) | ||
// result: (String, String, String) = (1,2,3) | ||
fetchThree.runA[Id] | ||
// [169] Many ToString OneAnd(1,List(2, 3)) | ||
// res5: cats.Id[(String, String, String)] = (1,2,3) | ||
``` | ||
|
||
## Concurrency | ||
## Parallelism | ||
|
||
If we combine two independent fetches from different data sources, the fetches will be run concurrently. First, let's add a data source that fetches a string's size. | ||
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. | ||
|
||
This time, instead of creating the results with `Query#sync` we are going to do it with `Query#async` for emulating an asynchronous data source. | ||
|
||
```scala | ||
implicit object LengthSource extends DataSource[String, Int]{ | ||
override def fetchOne(id: String): Eval[Option[Int]] = { | ||
Eval.later({ | ||
println(s"LengthSource $id") | ||
Option(id.size) | ||
override def fetchOne(id: String): Query[Option[Int]] = { | ||
Query.async((ok, fail) => { | ||
println(s"[${Thread.currentThread.getId}] One Length $id") | ||
ok(Option(id.size)) | ||
}) | ||
} | ||
override def fetchMany(ids: NonEmptyList[String]): Eval[Map[String, Int]] = { | ||
Eval.later({ | ||
println(s"LengthSource $ids") | ||
ids.unwrap.map(i => (i, i.size)).toMap | ||
override def fetchMany(ids: NonEmptyList[String]): Query[Map[String, Int]] = { | ||
Query.async((ok, fail) => { | ||
println(s"[${Thread.currentThread.getId}] Many Length $ids") | ||
ok(ids.unwrap.map(i => (i, i.size)).toMap) | ||
}) | ||
} | ||
} | ||
|
@@ -146,13 +152,13 @@ And now we can easily receive data from the two sources in a single fetch. | |
val fetchMulti: Fetch[(String, Int)] = (fetchString(1) |@| fetchLength("one")).tupled | ||
``` | ||
|
||
Note how the two independent data fetches are run concurrently, minimizing the latency cost of querying the two data sources. If our target monad was a concurrency monad like `Future`, they'd run in parallel, each in its own logical thread. | ||
Note how the two independent data fetches run in parallel, minimizing the latency cost of querying the two data sources. | ||
|
||
```scala | ||
val result: (String, Int) = fetchMulti.runA[Eval].value | ||
// ToStringSource OneAnd(1,List()) | ||
// LengthSource OneAnd(one,List()) | ||
// result: (String, Int) = (1,3) | ||
fetchMulti.runA[Id] | ||
// [169] One ToString 1 | ||
// [170] One Length one | ||
// res7: cats.Id[(String, Int)] = (1,3) | ||
``` | ||
|
||
## Caching | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's worth calling out in the opening paragraphs about identity, that you are using the identity and the thing that is fetched for the given the identity interchangeably throughout the examples. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch, i'll reword that since it'll probably cause confusion. |
||
|
@@ -169,8 +175,7 @@ val fetchTwice: Fetch[(String, String)] = for { | |
While running it, notice that the data source is only queried once. The next time the identity is requested it's served from the cache. | ||
|
||
```scala | ||
val result: (String, String) = fetchTwice.runA[Eval].value | ||
// ToStringSource 1 | ||
// result: (String, String) = (1,1) | ||
fetchTwice.runA[Id] | ||
// [169] One ToString 1 | ||
// res8: cats.Id[(String, String)] = (1,1) | ||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing that these versions will change once a new build is made from this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, these changes will be released in a 0.3.0 version