From 8fb8a9b7cb4800d6f83d43394dc230b399b5ff7a Mon Sep 17 00:00:00 2001 From: raulraja Date: Sat, 7 May 2016 21:57:35 +0200 Subject: [PATCH 1/7] Added Syntax for Id, Throwable and Fetch instances --- shared/src/main/scala/fetch.scala | 2 ++ shared/src/main/scala/syntax.scala | 38 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 shared/src/main/scala/syntax.scala diff --git a/shared/src/main/scala/fetch.scala b/shared/src/main/scala/fetch.scala index 7c2d8a5f..27ffdcc1 100644 --- a/shared/src/main/scala/fetch.scala +++ b/shared/src/main/scala/fetch.scala @@ -41,6 +41,8 @@ object `package` { type Fetch[A] = Free[FetchOp, A] + type FetchMonadError[M[_]] = MonadError[M, Throwable] + type FetchInterpreter[M[_]] = { type f[x] = StateT[M, FetchEnv, x] } diff --git a/shared/src/main/scala/syntax.scala b/shared/src/main/scala/syntax.scala new file mode 100644 index 00000000..ef37cbf1 --- /dev/null +++ b/shared/src/main/scala/syntax.scala @@ -0,0 +1,38 @@ +package fetch + +object syntax { + + /** Implicit syntax to lift any value to the context of Fetch via pure */ + implicit class FetchIdSyntax[A](val a: A) extends AnyVal { + + def fetch: Fetch[A] = + Fetch.pure(a) + + } + + /** Implicit syntax to lift exception to Fetch errors */ + implicit class FetchErrorSyntax[A <: Throwable](val a: A) extends AnyVal { + + def fetch: Fetch[A] = + Fetch.error(a) + + } + + /** Implicit syntax for Fetch ops in Free based Fetches */ + implicit class FetchSyntax[A](val fa: Fetch[A]) extends AnyVal { + + def join[B](fb: Fetch[B]): Fetch[(A, B)] = + Fetch.join(fa, fb) + + def runF[M[_]: FetchMonadError]: M[(FetchEnv, A)] = + Fetch.runFetch[M](fa) + + def runE[M[_]: FetchMonadError]: M[FetchEnv] = + Fetch.runEnv[M](fa) + + def run[M[_]: FetchMonadError]: M[A] = + Fetch.run[M](fa) + + } + +} From 1b371537d66b65c28ca424bbca6b5a24f83403c1 Mon Sep 17 00:00:00 2001 From: raulraja Date: Fri, 20 May 2016 12:43:58 +0200 Subject: [PATCH 2/7] Implicit syntax for fetches --- docs/src/tut/docs.md | 118 +++++++++++----- docs/src/tut/index.md | 10 +- shared/src/main/scala/fetch.scala | 10 +- shared/src/main/scala/implicits.scala | 17 --- shared/src/main/scala/syntax.scala | 39 ++++-- shared/src/test/scala/FetchTests.scala | 181 +++++++++++++++++-------- 6 files changed, 239 insertions(+), 136 deletions(-) diff --git a/docs/src/tut/docs.md b/docs/src/tut/docs.md index 1049e755..9f580cfe 100644 --- a/docs/src/tut/docs.md +++ b/docs/src/tut/docs.md @@ -118,8 +118,8 @@ When we are creating and combining `Fetch` values, we are just constructing a re dependencies. ```tut:silent -import cats.Id import fetch.implicits._ +import fetch.syntax._ val fetchUser: Fetch[User] = getUser(1) ``` @@ -137,14 +137,14 @@ Note that Fetch provides `MonadError` instances for a variety of different monad Let's run our first fetch! ```tut:book -val result: User = Fetch.run[Id](fetchUser) +val result: User = fetchUser.runA[Eval].value ``` In the previous examples, we: -- brought the implicit instance of `MonadError[Id, Throwable]` into scope importing `fetch.implicits._` +- brought the implicit instance of `MonadError[Eval, Throwable]` into scope importing `fetch.implicits._` - created a fetch for a `User` using the `getUser` function -- interpreted the fetch to a `Id[User]` (which is just a `User`) using `Fetch.run` +- interpreted the fetch to a `Eval[User]` (which is just a `User`) using the syntax `runA` that delegate to `Fetch.run` As you can see, the fetch was executed in one round to fetch the user and was finished after that. @@ -162,7 +162,7 @@ val fetchTwoUsers: Fetch[(User, User)] = for { When composing fetches with `flatMap` we are telling Fetch that the second one depends on the previous one, so it isn't able to make any optimizations. When running the above fetch, we will query the user data source in two rounds: one for the user with id 1 and another for the user with id 2. ```tut:book -val result: (User, User) = Fetch.run[Id](fetchTwoUsers) +val result: (User, User) = fetchTwoUsers.runA[Eval].value ``` ### Batching @@ -180,7 +180,7 @@ val fetchProduct: Fetch[(User, User)] = getUser(1).product(getUser(2)) Note how both ids (1 and 2) are requested in a single query to the data source when executing the fetch. ```tut:book -val result: (User, User) = Fetch.run[Id](fetchProduct) +val result: (User, User) = fetchProduct.runA[Eval].value ``` ### Deduplication @@ -194,7 +194,7 @@ val fetchDuped: Fetch[(User, User)] = getUser(1).product(getUser(1)) Note that when running the fetch, the identity 1 is only requested once even when it is needed by both fetches. ```tut:book -val result: (User, User) = Fetch.run[Id](fetchDuped) +val result: (User, User) = fetchDuped.runA[Eval].value ``` ### Caching @@ -214,7 +214,7 @@ val fetchCached: Fetch[(User, User)] = for { The above fetch asks for the same identity multiple times. Let's see what happens when executing it. ```tut:book -val result: (User, User) = Fetch.run[Id](fetchCached) +val result: (User, User) = fetchCached.runA[Eval].value ``` As you can see, the `User` with id 1 was fetched only once in a single round-trip. The next @@ -257,9 +257,9 @@ implicit object PostSource extends DataSource[PostId, Post]{ def getPost(id: PostId): Fetch[Post] = Fetch(id) val postInfoDatabase: Map[PostId, PostInfo] = Map( - 1 -> PostInfo("monad"), - 2 -> PostInfo("applicative"), - 3 -> PostInfo("monad") + 1 -> PostInfo("Run Wild, Run Free"), + 2 -> PostInfo("American Psycho"), + 3 -> PostInfo("Torrente 3") ) implicit object PostInfoSource extends DataSource[PostId, PostInfo]{ @@ -292,7 +292,7 @@ val fetchMulti: Fetch[(Post, User)] = for { We can now run the previous fetch, querying the posts data source first and the user data source afterwards. ```tut:book -val result: (Post, User) = Fetch.run[Id](fetchMulti) +val result: (Post, User) = fetchMulti.runA[Eval].value ``` In the previous example, we fetched a post given its id and then fetched its author. This @@ -316,7 +316,7 @@ val fetchConcurrent: Fetch[(Post, User)] = getPost(1).product(getUser(2)) The above example combines data from two different sources, and the library knows they are independent. ```tut:book -val result: (Post, User) = Fetch.run[Id](fetchConcurrent) +val result: (Post, User) = fetchConcurrent.runA[Eval].value ``` Since we are interpreting the fetch to the `Id` monad, that doesn't give us any parallelism; the fetches @@ -344,7 +344,7 @@ val fetchSequence: Fetch[List[User]] = List(getUser(1), getUser(2), getUser(3)). Since `sequence` uses applicative operations internally, the library is able to perform optimizations across all the sequenced fetches. ```tut:book -val result: List[User] = Fetch.run[Id](fetchSequence) +val result: List[User] = fetchSequence.runA[Eval].value ``` As you can see, requests to the user data source were batched, thus fetching all the data in one round. @@ -360,7 +360,7 @@ val fetchTraverse: Fetch[List[User]] = List(1, 2, 3).traverse(getUser) As you may have guessed, all the optimizations made by `sequence` still apply when using `traverse`. ```tut:book -val result: List[User] = Fetch.run[Id](fetchTraverse) +val result: List[User] = fetchTraverse.runA[Eval].value ``` # Interpreting a fetch to an async capable monad @@ -390,7 +390,7 @@ val fetchParallel: Fetch[(User, Post)] = (getUser(1) |@| getPost(1)).tupled We can now interpret a fetch into a future: ```tut:book -val fut: Future[(User, Post)] = Fetch.run[Future](fetchParallel) +val fut: Future[(User, Post)] = fetchParallel.runA[Future] Await.result(fut, 1 seconds) // this call blocks the current thread, don't do this at home! ``` @@ -415,7 +415,7 @@ val cache = InMemoryCache(UserSource.identity(1) -> User(1, "@dialelo")) We can pass a cache as the second argument when running a fetch with `Fetch.run`. ```tut:book -val result: User = Fetch.run[Id](fetchUser, cache) +val result: User = fetchUser.runA[Eval](cache).value ``` As you can see, when all the data is cached, no query to the data sources is executed since the results are available @@ -428,7 +428,7 @@ val fetchManyUsers: Fetch[List[User]] = List(1, 2, 3).traverse(getUser) If only part of the data is cached, the cached data won't be asked for: ```tut:book -val result: List[User] = Fetch.run[Id](fetchManyUsers, cache) +val result: List[User] = fetchManyUsers.runA[Eval](cache).value ``` ## Replaying a fetch without querying any data source @@ -436,15 +436,15 @@ val result: List[User] = Fetch.run[Id](fetchManyUsers, cache) When running a fetch, we are generally interested in its final result. However, we also have access to the cache and information about the executed rounds once we run a fetch. Fetch's interpreter keeps its state in an environment (implementing the `Env` trait), and we can get both the environment and result after running a fetch using `Fetch.runFetch` -instead of `Fetch.run`. +instead of `Fetch.run` or `value.runF` via it's implicit syntax. Knowing this, we can replay a fetch reusing the cache of a previous one. The replayed fetch won't have to call any of the data sources. ```tut:book -val populatedCache = Fetch.runEnv[Id](fetchManyUsers).cache +val populatedCache = fetchManyUsers.runE[Eval].value.cache -val result: List[User] = Fetch.run[Id](fetchManyUsers, populatedCache) +val result: List[User] = fetchManyUsers.runA[Eval](populatedCache).value ``` ## Implementing a custom cache @@ -481,7 +481,7 @@ val myCache = MyInMemoryCache(Map(UserSource.identity(1) -> User(1, "dialelo"))) We can now use our implementation of the cache when running a fetch. ```tut:book -val result: User = Fetch.run[Id](fetchUser, myCache) +val result: User = fetchUser.runA[Eval](myCache).value ``` # Error handling @@ -494,17 +494,19 @@ One of the most interesting combinators is `attempt`, which given a `M[A]` yield in the `Eval` monad to an `Xor` and not worry about exceptions. Let's create a fetch that always fails when executed: ```tut:silent -import cats.data.Xor -import fetch.implicits.evalMonadError - -val fetchError: Fetch[User] = Fetch.error(new Exception("Oh noes")) +val fetchError: Fetch[User] = (new Exception("Oh noes")).fetch ``` We can now use the Eval MonadError's `attempt` to convert a fetch result into a disjuntion and avoid throwing exceptions. ```tut:book -val result: Eval[User] = Fetch.run[Eval](fetchError) -val safeResult: Eval[Throwable Xor User] = evalMonadError.attempt(result) +import cats.data.Xor +import cats.MonadError + +val ME = implicitly[MonadError[Eval, Throwable]] + +val result: Eval[User] = fetchError.runA[Eval] +val safeResult: Eval[Throwable Xor User] = ME.attempt(result) val finalValue: Throwable Xor User = safeResult.value ``` @@ -522,9 +524,51 @@ about the execution of the fetch. # Syntax +## Implicit syntax + +Fetch provides implicit syntax to lift any value to the context of a `Fetch` in addition to the most common used +combinators active within `Fetch` instances. + +### pure + +Plain values can be lifted to the Fetch monad with `value.fetch`: + +```tut:silent +val fetchPure: Fetch[Int] = 42.fetch +``` + +Executing a pure fetch doesn't query any data source, as expected. + +```tut:book +val result: Int = fetchPure.runA[Eval].value +``` + +### error + +Errors can also be lifted to the Fetch monad via `exception.fetch`. Note that interpreting +an errorful fetch to `Eval` will throw the exception so we won't do that: + +```tut:silent +val fetchFail: Fetch[Int] = (new Exception("Something went terribly wrong")).fetch[Int] +``` + +### join + +We can compose two independent fetches with `fetch1.join(fetch2)`. + +```tut:silent +val fetchJoined: Fetch[(Post, User)] = getPost(1).join(getUser(2)) +``` + +If the fetches are to the same data source they will be batched; if they aren't, they will be evaluated at the same time. + +```tut:book +val result: (Post, User) = fetchJoined.runA[Eval].value +``` + ## Companion object -We've been using cats' syntax throughout the examples since it's more concise and general than the +We've been using `cats.syntax' and `fetch.syntax` throughout the examples since it's more concise and general than the methods in the `Fetch` companion object. However, you can use the methods in the companion object directly. @@ -541,13 +585,13 @@ val fetchPure: Fetch[Int] = Fetch.pure(42) Executing a pure fetch doesn't query any data source, as expected. ```tut:book -val result: Int = Fetch.run[Id](fetchPure) +val result: Int = Fetch.run[Eval](fetchPure).value ``` ### error Errors can also be lifted to the Fetch monad, in this case with `Fetch#error`. Note that interpreting -an errorful fetch to `Id` will throw the exception so we won't do that: +an errorful fetch to `Eval` will throw the exception so we won't do that: ```tut:silent val fetchFail: Fetch[Int] = Fetch.error(new Exception("Something went terribly wrong")) @@ -564,7 +608,7 @@ val fetchJoined: Fetch[(Post, User)] = Fetch.join(getPost(1), getUser(2)) If the fetches are to the same data source they will be batched; if they aren't, they will be evaluated at the same time. ```tut:book -val result: (Post, User) = Fetch.run[Id](fetchJoined) +val result: (Post, User) = Fetch.run[Eval](fetchJoined).value ``` ### sequence @@ -579,7 +623,7 @@ val fetchSequence: Fetch[List[User]] = Fetch.sequence(List(getUser(1), getUser(2 Note that `Fetch#sequence` is not as general as the `sequence` method from `Traverse`, but performs the same optimizations. ```tut:book -val result: List[User] = Fetch.run[Id](fetchSequence) +val result: List[User] = Fetch.run[Eval](fetchSequence).value ``` ### traverse @@ -593,7 +637,7 @@ val fetchTraverse: Fetch[List[User]] = Fetch.traverse(List(1, 2, 3))(getUser) Note that `Fetch#traverse` is not as general as the `traverse` method from `Traverse`, but performs the same optimizations. ```tut:book -val result: List[User] = Fetch.run[Id](fetchTraverse) +val result: List[User] = Fetch.run[Eval](fetchTraverse).value ``` ## cats @@ -624,7 +668,7 @@ val fetchThree: Fetch[(Post, User, Post)] = (getPost(1) |@| getUser(2) |@| getPo Notice how the queries to posts are batched. ```tut:book -val result: (Post, User, Post) = Fetch.run[Id](fetchThree) +val result: (Post, User, Post) = fetchThree.runA[Eval].value ``` More interestingly, we can use it to apply a pure function to the results of various @@ -635,7 +679,7 @@ val fetchFriends: Fetch[String] = (getUser(1) |@| getUser(2)).map({ (one, other) s"${one.username} is friends with ${other.username}" }) -val result: String = Fetch.run[Id](fetchFriends) +val result: String = fetchFriends.runA[Eval].value ``` The above example is equivalent to the following using the `Fetch#join` method: @@ -645,7 +689,7 @@ val fetchFriends: Fetch[String] = Fetch.join(getUser(1), getUser(2)).map({ case s"${one.username} is friends with ${other.username}" }) -val result: String = Fetch.run[Id](fetchFriends) +val result: String = fetchFriends.runA[Eval].value ``` # Resources diff --git a/docs/src/tut/index.md b/docs/src/tut/index.md index d97fe5b4..6c1c447c 100644 --- a/docs/src/tut/index.md +++ b/docs/src/tut/index.md @@ -66,8 +66,8 @@ 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. ```tut:silent -import cats.Id import fetch.implicits._ +import fetch.syntax._ val fetchOne: Fetch[String] = fetchString(1) ``` @@ -75,7 +75,7 @@ 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 (`Id` in our example) needs to implement `MonadError[M, Throwable]`, we provide an instance for `Id` in `fetch.implicits._`, that's why we imported it. ```tut:book -val result: String = Fetch.run[Id](fetchOne) +val result: String = fetchOne.runA[Eval].value ``` As you can see in the previous example, the `ToStringSource` is queried once to get the value of 1. @@ -93,7 +93,7 @@ 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. ```tut:book -val result: (String, String, String) = Fetch.run[Id](fetchThree) +val result: (String, String, String) = fetchThree.runA[Eval].value ``` ## Concurrency @@ -122,7 +122,7 @@ val fetchMulti: Fetch[(String, Int)] = (fetchString(1) |@| fetchLength("one")).t 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. ```tut:book -val result: (String, Int) = Fetch.run[Id](fetchMulti) +val result: (String, Int) = fetchMulti.runA[Eval].value ``` ## Caching @@ -139,6 +139,6 @@ val fetchTwice: Fetch[(String, String)] = for { When running it, notice that the data source is only queried once. The next time the identity is requested it's served from the cache. ```tut:book -val result: (String, String) = Fetch.run[Id](fetchTwice) +val result: (String, String) = fetchTwice.runA[Eval].value ``` diff --git a/shared/src/main/scala/fetch.scala b/shared/src/main/scala/fetch.scala index 27ffdcc1..dbf26004 100644 --- a/shared/src/main/scala/fetch.scala +++ b/shared/src/main/scala/fetch.scala @@ -220,10 +220,7 @@ object `package` { class FetchRunnerEnv[M[_]] { - def apply[A]( - fa: Fetch[A], - cache: DataSourceCache = InMemoryCache.empty - )( + def apply[A](fa: Fetch[A], cache: DataSourceCache = InMemoryCache.empty)( implicit MM: MonadError[M, Throwable] ): M[FetchEnv] = fa.foldMap[FetchInterpreter[M]#f](interpreter).runS(FetchEnv(cache)) @@ -231,10 +228,7 @@ object `package` { class FetchRunnerA[M[_]] { - def apply[A]( - fa: Fetch[A], - cache: DataSourceCache = InMemoryCache.empty - )( + def apply[A](fa: Fetch[A], cache: DataSourceCache = InMemoryCache.empty)( implicit MM: MonadError[M, Throwable] ): M[A] = fa.foldMap[FetchInterpreter[M]#f](interpreter).runA(FetchEnv(cache)) diff --git a/shared/src/main/scala/implicits.scala b/shared/src/main/scala/implicits.scala index 0db5d57b..0127496b 100644 --- a/shared/src/main/scala/implicits.scala +++ b/shared/src/main/scala/implicits.scala @@ -61,21 +61,4 @@ object implicits { } }) } - - implicit val idMonadError: MonadError[Id, Throwable] = new MonadError[Id, Throwable] { - override def pure[A](x: A): Id[A] = x - - override def map[A, B](fa: Id[A])(f: A ⇒ B): Id[B] = f(fa) - - override def flatMap[A, B](fa: Id[A])(ff: A => Id[B]): Id[B] = ff(fa) - - override def raiseError[A](e: Throwable): Id[A] = throw e - - override def handleErrorWith[A](fa: Id[A])(f: Throwable ⇒ Id[A]): Id[A] = - try { - fa - } catch { - case e: Throwable => f(e) - } - } } diff --git a/shared/src/main/scala/syntax.scala b/shared/src/main/scala/syntax.scala index ef37cbf1..9632fab2 100644 --- a/shared/src/main/scala/syntax.scala +++ b/shared/src/main/scala/syntax.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2016 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package fetch object syntax { @@ -7,15 +23,13 @@ object syntax { def fetch: Fetch[A] = Fetch.pure(a) - } /** Implicit syntax to lift exception to Fetch errors */ implicit class FetchErrorSyntax[A <: Throwable](val a: A) extends AnyVal { - def fetch: Fetch[A] = - Fetch.error(a) - + def fetch[B]: Fetch[B] = + Fetch.error[B](a) } /** Implicit syntax for Fetch ops in Free based Fetches */ @@ -25,14 +39,21 @@ object syntax { Fetch.join(fa, fb) def runF[M[_]: FetchMonadError]: M[(FetchEnv, A)] = - Fetch.runFetch[M](fa) + Fetch.runFetch[M](fa, InMemoryCache.empty) def runE[M[_]: FetchMonadError]: M[FetchEnv] = - Fetch.runEnv[M](fa) + Fetch.runEnv[M](fa, InMemoryCache.empty) - def run[M[_]: FetchMonadError]: M[A] = - Fetch.run[M](fa) + def runA[M[_]: FetchMonadError]: M[A] = + Fetch.run[M](fa, InMemoryCache.empty) - } + def runF[M[_]: FetchMonadError](cache: DataSourceCache): M[(FetchEnv, A)] = + Fetch.runFetch[M](fa, cache) + + def runE[M[_]: FetchMonadError](cache: DataSourceCache): M[FetchEnv] = + Fetch.runEnv[M](fa, cache) + def runA[M[_]: FetchMonadError](cache: DataSourceCache): M[A] = + Fetch.run[M](fa, cache) + } } diff --git a/shared/src/test/scala/FetchTests.scala b/shared/src/test/scala/FetchTests.scala index e20afc8f..957eafe2 100644 --- a/shared/src/test/scala/FetchTests.scala +++ b/shared/src/test/scala/FetchTests.scala @@ -20,9 +20,12 @@ import cats.{Eval, Id, MonadError} import fetch._ -class FetchTests extends FreeSpec with Matchers { - implicit val M: MonadError[Eval, Throwable] = implicits.evalMonadError - implicit val I: MonadError[Id, Throwable] = implicits.idMonadError +object TestHelper { + + import fetch.implicits._ + import fetch.syntax._ + + val M: MonadError[Eval, Throwable] = implicits.evalMonadError case class NotFound() extends Throwable @@ -83,9 +86,90 @@ class FetchTests extends FreeSpec with Matchers { rs.filter( r => r.kind match { - case ConcurrentRound(_) => true - case other => false - }) + case ConcurrentRound(_) => true + case other => false + } + ) +} + +class FetchSyntaxTests extends FreeSpec with Matchers { + + import fetch.implicits._ + import fetch.syntax._ + import TestHelper._ + + "Cartesian syntax is implicitly concurrent" in { + import cats.syntax.cartesian._ + + val fetch: Fetch[(Int, List[Int])] = (one(1) |@| many(3)).tupled + + val env = Fetch.runEnv[Eval](fetch).value + val rounds = env.rounds + + concurrent(rounds).size shouldEqual 1 + } + + "Apply syntax is implicitly concurrent" in { + import cats.syntax.apply._ + + val fetch: Fetch[Int] = Fetch.pure((x: Int, y: Int) => x + y).ap2(one(1), one(2)) + + val env = Fetch.runEnv[Eval](fetch).value + val rounds = env.rounds + + concurrent(rounds).size shouldEqual 1 + totalBatches(rounds) shouldEqual 1 + totalFetched(rounds) shouldEqual 2 + } + + "`fetch` syntax allows lifting of any value to the context of a fetch" in { + Fetch.pure(42) shouldEqual 42.fetch + } + + "`fetch` syntax allows lifting of any `Throwable` as a failure on a fetch" in { + case object Ex extends RuntimeException + val ME = implicitly[MonadError[Eval, Throwable]] + val e1 = ME.attempt(Fetch.run[Eval](Fetch.error(Ex))).value + val e2 = ME.attempt(Fetch.run[Eval](Ex.fetch)).value + e1 shouldEqual e2 + } + + "`join` syntax is equivalent to `Fetch#join`" in { + val join1 = Fetch.join(one(1), many(3)).runA[Eval].value + val join2 = one(1).join(many(3)).runA[Eval].value + + join1 shouldEqual join2 + } + + "`runF` syntax is equivalent to `Fetch#runFetch`" in { + + val rf1 = Fetch.runFetch(1.fetch).value + val rf2 = 1.fetch.runF[Eval].value + + rf1 shouldEqual rf2 + } + + "`runE` syntax is equivalent to `Fetch#runEnv`" in { + + val rf1 = Fetch.runEnv(1.fetch).value + val rf2 = 1.fetch.runE[Eval].value + + rf1 shouldEqual rf2 + } + + "`runA` syntax is equivalent to `Fetch#run`" in { + + val rf1 = Fetch.run(1.fetch).value + val rf2 = 1.fetch.runA[Eval].value + + rf1 shouldEqual rf2 + } +} + +class FetchTests extends FreeSpec with Matchers { + + import fetch.implicits._ + import TestHelper._ "We can lift plain values to Fetch" in { val fetch: Fetch[Int] = Fetch.pure(42) @@ -107,14 +191,6 @@ class FetchTests extends FreeSpec with Matchers { } } - "Concurrent data sources with errors throw fetch failures" in { - val fetch: Fetch[(Int, Int)] = Fetch.join(one(1), Fetch(Never())) - - intercept[FetchFailure[InMemoryCache]] { - Fetch.runEnv[Eval](fetch).value - } - } - "Data sources with errors throw fetch failures that can be handled" in { val fetch: Fetch[Int] = Fetch(Never()) @@ -196,7 +272,7 @@ class FetchTests extends FreeSpec with Matchers { Fetch.run[Eval](fetch).value shouldEqual (1, List(0, 1, 2)) } - "We can use Fetch as an cartesian" in { + "We can use Fetch as a cartesian" in { import cats.syntax.cartesian._ val fetch: Fetch[(Int, List[Int])] = (one(1) |@| many(3)).tupled @@ -360,30 +436,6 @@ class FetchTests extends FreeSpec with Matchers { totalBatches(concurrent(rounds)) shouldEqual 1 } - "Cartesian syntax is implicitly concurrent" in { - import cats.syntax.cartesian._ - - val fetch: Fetch[(Int, List[Int])] = (one(1) |@| many(3)).tupled - - val env = Fetch.runEnv[Eval](fetch).value - val rounds = env.rounds - - concurrent(rounds).size shouldEqual 1 - } - - "Apply syntax is implicitly concurrent" in { - import cats.syntax.apply._ - - val fetch: Fetch[Int] = Fetch.pure((x: Int, y: Int) => x + y).ap2(one(1), one(2)) - - val env = Fetch.runEnv[Eval](fetch).value - val rounds = env.rounds - - concurrent(rounds).size shouldEqual 1 - totalBatches(rounds) shouldEqual 1 - totalFetched(rounds) shouldEqual 2 - } - "We can depend on previous computations of Fetch values" in { val fetch: Fetch[Int] = for { o <- one(1) @@ -430,11 +482,13 @@ class FetchTests extends FreeSpec with Matchers { val fetch: Fetch[List[Int]] = Fetch.sequence(sources) val rounds = Fetch - .runEnv[Eval](fetch, - InMemoryCache( - OneSource.identity(One(1)) -> 1, - OneSource.identity(One(2)) -> 2 - )) + .runEnv[Eval]( + fetch, + InMemoryCache( + OneSource.identity(One(1)) -> 1, + OneSource.identity(One(2)) -> 2 + ) + ) .value .rounds @@ -509,12 +563,14 @@ class FetchTests extends FreeSpec with Matchers { } yield aOne + anotherOne val rounds = Fetch - .runEnv[Eval](fetch, - InMemoryCache( - OneSource.identity(One(1)) -> 1, - OneSource.identity(One(2)) -> 2, - OneSource.identity(One(3)) -> 3 - )) + .runEnv[Eval]( + fetch, + InMemoryCache( + OneSource.identity(One(1)) -> 1, + OneSource.identity(One(2)) -> 2, + OneSource.identity(One(3)) -> 3 + ) + ) .value .rounds @@ -534,7 +590,8 @@ class FetchTests extends FreeSpec with Matchers { OneSource.identity(One(3)) -> 3, OneSource.identity(One(1)) -> 1, ManySource.identity(Many(2)) -> List(0, 1) - )) + ) + ) "We can use a custom cache" in { val fetch = for { @@ -549,13 +606,15 @@ class FetchTests extends FreeSpec with Matchers { } yield aOne + anotherOne val rounds = Fetch - .runEnv[Eval](fetch, - InMemoryCache( - OneSource.identity(One(1)) -> 1, - OneSource.identity(One(2)) -> 2, - OneSource.identity(One(3)) -> 3, - ManySource.identity(Many(2)) -> List(0, 1) - )) + .runEnv[Eval]( + fetch, + InMemoryCache( + OneSource.identity(One(1)) -> 1, + OneSource.identity(One(2)) -> 2, + OneSource.identity(One(3)) -> 3, + ManySource.identity(Many(2)) -> List(0, 1) + ) + ) .value .rounds @@ -633,7 +692,8 @@ class FetchFutureTests extends AsyncFreeSpec with Matchers { Article(1, "An article with id 1"), Article(1, "An article with id 1"), Article(2, "An article with id 2") - )) + ) + ) } "We can use combinators and multiple sources in a for comprehension and interpret a fetch into a future" in { @@ -656,6 +716,7 @@ class FetchFutureTests extends AsyncFreeSpec with Matchers { Author(2, "@egg2"), Author(3, "@egg3") ) - )) + ) + ) } } From 23b2c5fccc660fd926b2b1f2b3301376666d98ff Mon Sep 17 00:00:00 2001 From: raulraja Date: Fri, 20 May 2016 12:57:08 +0200 Subject: [PATCH 3/7] Added docs about runA, runE and runF --- docs/src/tut/docs.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/src/tut/docs.md b/docs/src/tut/docs.md index 9f580cfe..eeb4914e 100644 --- a/docs/src/tut/docs.md +++ b/docs/src/tut/docs.md @@ -566,6 +566,30 @@ If the fetches are to the same data source they will be batched; if they aren't, val result: (Post, User) = fetchJoined.runA[Eval].value ``` +### runA + +Run directly any fetch to a target any target `Monad` with a `MonadError` instance in scope `fetch1.runA[Eval]`. + +```tut:silent +val post: Eval[Post] = getPost(1).runA[Eval] +``` + +### runE + +Extract a fetch an get it's runtime environment `fetch1.runE[Eval]`. + +```tut:silent +val env: Eval[FetchEnv] = getPost(1).runE[Eval] +``` + +### runF + +Run a fetch obtaining the environment and final value `fetch1.runF[Eval]`. + +```tut:silent +val env: Eval[(FetchEnv, Post)] = getPost(1).runF[Eval] +``` + ## Companion object We've been using `cats.syntax' and `fetch.syntax` throughout the examples since it's more concise and general than the From a318f5cf4e7bb0062862505875b834d1011d64c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sat, 21 May 2016 12:45:34 +0200 Subject: [PATCH 4/7] Test missing identities --- shared/src/test/scala/FetchTests.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/shared/src/test/scala/FetchTests.scala b/shared/src/test/scala/FetchTests.scala index 957eafe2..0704b48c 100644 --- a/shared/src/test/scala/FetchTests.scala +++ b/shared/src/test/scala/FetchTests.scala @@ -338,6 +338,22 @@ class FetchTests extends FreeSpec with Matchers { } } + "If there is a missing identity in the left hand of a product the product will fail" in { + val fetch: Fetch[(Int, List[Int])] = Fetch.join(Fetch(Never()), many(3)) + + intercept[FetchFailure[InMemoryCache]] { + Fetch.run[Eval](fetch).value + } + } + + "If there is a missing identity in the right hand of a product the product will fail" in { + val fetch: Fetch[(List[Int], Int)] = Fetch.join(many(3), Fetch(Never())) + + intercept[FetchFailure[InMemoryCache]] { + Fetch.run[Eval](fetch).value + } + } + "The product of concurrent fetches implies everything fetched concurrently" in { val fetch = Fetch.join( Fetch.join( From 3a7e3285b0800977c2b2a9b0613639627211cbf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sat, 21 May 2016 12:56:17 +0200 Subject: [PATCH 5/7] Reword the description of `error` syntax --- docs/src/tut/docs.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/src/tut/docs.md b/docs/src/tut/docs.md index eeb4914e..bf9d826c 100644 --- a/docs/src/tut/docs.md +++ b/docs/src/tut/docs.md @@ -615,10 +615,17 @@ val result: Int = Fetch.run[Eval](fetchPure).value ### error Errors can also be lifted to the Fetch monad, in this case with `Fetch#error`. Note that interpreting -an errorful fetch to `Eval` will throw the exception so we won't do that: +an errorful fetch to `Eval` won't throw the exception unless we access the value with the `.value` method. + +A safer way to deal with errors is to use MonadError's `attempt` to turn the exception into a `Xor.Left` value: + +```tut:book +val ME = implicitly[MonadError[Eval, Throwable]] -```tut:silent val fetchFail: Fetch[Int] = Fetch.error(new Exception("Something went terribly wrong")) +val result: Eval[Int] = fetchFail.runA[Eval] +val safeResult: Eval[Throwable Xor Int] = ME.attempt(result) +val finalValue: Throwable Xor Int = safeResult.value ``` ### join From dbdc6b3f59d30820a2961ceaf6f028af2a9b5639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sat, 21 May 2016 13:54:14 +0200 Subject: [PATCH 6/7] Minor edits --- docs/src/tut/docs.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/tut/docs.md b/docs/src/tut/docs.md index bf9d826c..0903bb1f 100644 --- a/docs/src/tut/docs.md +++ b/docs/src/tut/docs.md @@ -144,7 +144,7 @@ In the previous examples, we: - brought the implicit instance of `MonadError[Eval, Throwable]` into scope importing `fetch.implicits._` - created a fetch for a `User` using the `getUser` function -- interpreted the fetch to a `Eval[User]` (which is just a `User`) using the syntax `runA` that delegate to `Fetch.run` +- interpreted the fetch to a `Eval[User]` using the syntax `runA` that delegate to `Fetch.run` As you can see, the fetch was executed in one round to fetch the user and was finished after that. @@ -394,8 +394,7 @@ val fut: Future[(User, Post)] = fetchParallel.runA[Future] Await.result(fut, 1 seconds) // this call blocks the current thread, don't do this at home! ``` -Since futures run in a thread pool, we need to explicitly set println output to the standard output. Note how both requests -to the data sources run in parallel, each in its own logical thread. +Since futures run in a thread pool, both requests to the data sources run in parallel, each in its own logical thread. # Caching From b9745f57ab5840c1ea0a39dc4419df2d9c86459f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sat, 21 May 2016 13:57:42 +0200 Subject: [PATCH 7/7] Edit error syntax example --- docs/src/tut/docs.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/src/tut/docs.md b/docs/src/tut/docs.md index 0903bb1f..94271ef7 100644 --- a/docs/src/tut/docs.md +++ b/docs/src/tut/docs.md @@ -545,10 +545,17 @@ val result: Int = fetchPure.runA[Eval].value ### error Errors can also be lifted to the Fetch monad via `exception.fetch`. Note that interpreting -an errorful fetch to `Eval` will throw the exception so we won't do that: +an errorful fetch to `Eval` won't throw the exception unless we access the value with the `.value` method. + +A safer way to deal with errors is to use MonadError's `attempt` to turn the exception into a `Xor.Left` value: ```tut:silent +val ME = implicitly[MonadError[Eval, Throwable]] + val fetchFail: Fetch[Int] = (new Exception("Something went terribly wrong")).fetch[Int] +val result: Eval[Int] = fetchFail.runA[Eval] +val safeResult: Eval[Throwable Xor Int] = ME.attempt(result) +val finalValue: Throwable Xor Int = safeResult.value ``` ### join