Permalink
Browse files

Error Handling + Syntax sections

  • Loading branch information...
raulraja committed Jul 4, 2016
1 parent 1c10aef commit 3646bbfe56e8eb325b159a636205ecdf6a554c5f
@@ -52,4 +52,71 @@ object CachingSection extends FlatSpec with Matchers with exercise.Section {
env.rounds.size should be(res0)
}

/**
* = Replaying a fetch without querying any data source =
*
* 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` 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.
*
*/
def replaying(res0: Int, res1: Int) = {
def fetchUsers = List(1, 2, 3).traverse(getUser)
val firstEnv = fetchUsers.runE[Id]

firstEnv.rounds.size should be(res0)

val secondEnv = fetchUsers.runA[Id](firstEnv.cache)

firstEnv.rounds.size should be(res1)
}

/**
*
* ## Implementing a custom cache
*
* The default cache is implemented as an immutable in-memory map,
* but users are free to use their own caches when running a fetch.
* Your cache should implement the `DataSourceCache` trait,
* and after that you can pass it to Fetch's `run` methods.
*
* There is no need for the cache to be mutable since fetch
* executions run in an interpreter that uses the state monad.
* Note that the `update` method in the `DataSourceCache` trait
* yields a new, updated cache.
*
* ```scala
* trait DataSourceCache {
* def update[A](k: DataSourceIdentity, v: A): DataSourceCache
* def get[A](k: DataSourceIdentity): Option[A]
* }
* ```
*
* Let's implement a cache that forgets everything we store in it.
*
* ```tut:silent
* final case class ForgetfulCache() extends DataSourceCache {
* override def get[A](k: DataSourceIdentity): Option[A] = None
* override def update[A](k: DataSourceIdentity, v: A): ForgetfulCache = this
* }
* ```
*
* We can now use our implementation of the cache when running a fetch.
*/
def customCache(res0: Int) = {

val fetchSameTwice: Fetch[(User, User)] = for {
one <- getUser(1)
another <- getUser(1)
} yield (one, another)

val env = fetchSameTwice.runE[Id](ForgetfulCache())

env.rounds.size should be(res0)
}

}
@@ -0,0 +1,67 @@
package fetchlib

import cats.data.{NonEmptyList, Xor}
import org.scalatest._
import fetch._

import cats._
import fetch.unsafe.implicits._
import fetch.syntax._
import scala.util.Try

/**
*
* = Error handling =
*
* Fetch is used for reading data from remote sources and the queries we perform
* can and will fail at some point.
*
* @param name error_handling
*/
object ErrorHandlingSection extends FlatSpec with Matchers with exercise.Section {

import FetchTutorialHelper._

/**
* What happens if we run a fetch and fails? We'll create a fetch that always fails to learn about it.
* {{{
* val fetchError: Fetch[User] = (new Exception("Oh noes")).fetch
* }}}
*/
def failedFetch(res0: Boolean) = {
Try(fetchError.runA[Id]).isFailure should be(res0)
}

/**
* Since `Id` runs the fetch eagerly, the only way to recover from errors is to capture the exception.
* We'll use Cats' `Eval` type as the target monad which, instead of evaluating the fetch eagerly, gives us
* an `Eval[A]` that we can run anytime with its `.value` method.
*
* We can use the `FetchMonadError[Eval]#attempt` to convert a fetch result into a disjuntion and avoid
* throwing exceptions. Fetch provides an implicit instance of `FetchMonadError[Eval]` that we can import
* from `fetch.unsafe.implicits._` to have it available.
*
* Now we can convert `Eval[User]` into `Eval[Throwable Xor User]` and capture exceptions as values
* in the left of the disjunction.
*/
def attemptFailedFetch(res0: Exception Xor User) = {
import fetch.unsafe.implicits._
import cats.Eval
import cats.data.Xor

val safeResult: Eval[Throwable Xor User] =
FetchMonadError[Eval].attempt(fetchError.runA[Eval])

safeResult.value should be(res0)
}

/**
* And more succintly with Cats' applicative error syntax.
*/
def attemptFailedFetchSyntax(res0: Exception Xor User) = {
import cats.syntax.applicativeError._

fetchError.runA[Eval].attempt.value should be(res0)
}

}
@@ -12,6 +12,8 @@ object FetchLibrary extends exercise.Library {

override def sections = List(
UsageSection,
CachingSection
CachingSection,
ErrorHandlingSection,
SyntaxSection
)
}
@@ -87,4 +87,13 @@ object FetchTutorialHelper {

val cache = InMemoryCache(UserSource.identity(1) -> User(1, "@dialelo"))

final case class ForgetfulCache() extends DataSourceCache {
override def get[A](k: DataSourceIdentity): Option[A] = None
override def update[A](k: DataSourceIdentity, v: A): ForgetfulCache = this
}

import fetch.syntax._

val fetchError: Fetch[User] = (new Exception("Oh noes")).fetch

}
@@ -0,0 +1,92 @@
package fetchlib

import cats.data.{NonEmptyList, Xor}
import org.scalatest._
import fetch._

import cats._
import fetch.unsafe.implicits._
import fetch.syntax._
import scala.util.Try

/**
* = Syntax =
*
* @param name syntax
*/
object SyntaxSection extends FlatSpec with Matchers with exercise.Section {

import FetchTutorialHelper._

/**
* = 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`:
*
* Executing a pure fetch doesn't query any data source, as expected.
*
*/
def implicitSyntax(res0: Int) = {
42.fetch.runA[Id] should be(res0)
}

/**
* = error =
*
* Errors can also be lifted to the Fetch monad via `exception.fetch`.
*
*/
def errorSyntax(res0: Boolean) = {
val fetchFail: Fetch[Int] = new Exception("Something went terribly wrong").fetch
Try(fetchFail.runA[Id]).isFailure should be(res0)
}

/**
* = join =
*
* We can compose two independent fetches with `fetch1.join(fetch2)`.
*/
def join(res0: Tuple2[Post, User]) = {
val fetchJoined: Fetch[(Post, User)] = getPost(1).join(getUser(2))
fetchJoined.runA[Id] should be(res0)
}

/**
* = runA =
*
* Run directly any fetch with `fetch1.runA[M[_]]`.
*
*/
def runA(res0: Int) = {
1.fetch.runA[Id] should be(res0)
}

/**
* = runE =
*
* Discard results and get the fetch environment with `fetch1.runE[M[_]]`.
*
*/
def runE(res0: Boolean) = {
1.fetch.runE[Id].isInstanceOf[FetchEnv] should be(res0)
}

/**
* = runF =
*
* Obtain results and get the fetch environment with `fetch1.runF[M[_]]`.
*
*/
def runF(res0: Int, res1: Boolean) = {
val (env, result) = 1.fetch.runF[Id]

result should be(res0)
env.isInstanceOf[FetchEnv] should be(res1)
}

}

0 comments on commit 3646bbf

Please sign in to comment.