Skip to content
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

Bring Your Own Effect #145

Merged
merged 43 commits into from
Nov 7, 2017
Merged

Bring Your Own Effect #145

merged 43 commits into from
Nov 7, 2017

Conversation

cb372
Copy link
Owner

@cb372 cb372 commented Oct 26, 2017

It's two years since I left a comment about how I should put my clever trousers on, and a whole month since I started this branch, but it's finally here and ready for review*.

*I ended up touching nearly every file in the project, so I don't expect anyone to actually review the diff!

The headline

Bring Your Own Effect.

In other words, ScalaCache's API is now parameterised by a higher-kinded type F[_], which is the effect monad in which the computation is wrapped. You, the user, can decide whether you want ScalaCache to return a Future, a Try, a Scalaz Task, a plain old value, or whatever else your heart desires.

Wherever a function previously looked like

def foo[A](...): A

or

def foo[A](...): Future[A]

, it now looks like

def foo[F[_], A](...)(implicit mode: Mode[F]): F[A]

Modes

The effect F[_] is decided by a "mode", which is passed implicitly.

Until now ScalaCache has supported both synchronous blocking execution and Scala Future, albeit in a rather ad-hoc and inconsistent fashion. In order to continue the support for these two execution styles, ScalaCache comes with a few modes out of the box:

Sync

For simple, blocking execution on the current thread, throwing exceptions in the case of errors, import the sync mode:

import scalacache.modes.sync._

Scala Future

To wrap everything in a Future, import the appropriate mode, along with an execution context:

import scalacache.modes.scalaFuture._
import scala.concurrent.ExecutionContext.Implicits.Global

Try

A mode to wrap things in Try is also provided out of the box:

import scalacache.modes.try_._

More exotic modes: cats-effect, Monix, Scalaz

There are also modes for cats-effect IO, Monix Task and Scalaz Task, each provided in separate modules.

e.g.

import scalacache.Monix.modes._

These differ from the aforementioned modes in that they are lazy: they defer the computation until they are specifically told to run.

Note: ScalaCache core is politically neutral, having no dependencies on either Cats or Scalaz.

Safer, DRYer, more sensible API

Until now ScalaCache has made type safety optional. It had two different APIs: a type-safe one and a more loosely typed one. Using the latter, it is possible to write a value to the cache as a Foo and then try to read it back out as a Bar, leading to a runtime error.

As I've grown older and wiser, I've come to appreciate that this was a poor design decision. The existence of two different, but very similar, APIs, was inelegant, confusing for users and annoying to maintain.

In this PR, I've got rid of the loosely typed API, meaning ScalaCache can only be used in one way. Less confusion, less duplication.

As before, the intended usage of ScalaCache is via the API exposed in the package object:

import scalacache._

But now, instead of a ScalaCache instance, you will need a Cache instance in scope. Cache is parameterised by the type of the values it stores:

implicit val fooCache: Cache[Foo] = CaffeineCache[Foo]

This means that you can only store values of type Foo in that cache.

You'll also need a mode in scope:

import scalacache.modes.try_._

Then you'll be able to call the methods on the package object in a type-safe way:

val result: Try[Option[Foo]] = get("cache key")

Codec

In the existing ScalaCache API, a serialization codec is passed implicitly into every function where it is needed. But this means it is possible to e.g. write a value to the cache with one codec and then try to read it back with another, leading to a runtime error.

Just as a cache should be parameterised by the type of values it stores, it also makes more sense to pass in the codec just once, when the cache is constructed. The new API design allows us to do that.

Example usage

Here is an example of using ScalaCache with Caffeine and Scalaz Task.

import scalacache._
import scalacache.caffeine._
import scalacache.Scalaz72.modes._

implicit val cache: Cache[String] = CaffeineCache[String]

val key = UUID.randomUUID().toString
val initialValue = UUID.randomUUID().toString

val program: Task[Option[String]] =
  for {
    _ <- put(key)(initialValue)
    readFromCache <- get(key)
    updatedValue = "prepended " + readFromCache.getOrElse("couldn't find in cache!")
    _ <- put(key)(updatedValue)
    finalValueFromCache <- get(key)
  } yield finalValueFromCache

Sync API

As part of this work I really wanted to get rid of the "sync" API, which is a copy of the API that returns plain old values not wrapped in any effect.

Unfortunately Scala's type inference didn't want to cooperate, so the sync API is still there. All it does is call the normal API, setting F[_] to Id[_].

Example usage:

import scalacache._
import scalacache.caffeine._
import scalacache.modes.sync._

implicit val cache: Cache[String] = CaffeineCache[String]

val value: Option[String] = scalacache.sync.get("key")

Similarly, memoizeSync (the sync version of memoize) is still there.

Other breaking changes

As I was already substantially changing the API, I took the opportunity to clean up a few years' worth of accumulated cruft:

  • Removed some overloaded methods in order to make a more minimal/canonical API. e.g. cachingWithTtl and the overloads of memoize are gone.
  • Removed legacy codec support from Redis and Memcached
  • Simplified Codec: the underlying representation type is now hardcoded to Array[Byte]]

Benchmarks

TODO: run the benchmarks on this branch and on master, and post the results here.

Versioning and roadmap

The current version of ScalaCache is 0.10.x.

Assuming there isn't massive resistance to this PR and it actually gets merged, it will be released as 0.20.x. The version jump is to represent the huge number of breaking changes in the PR.

After that I will start working towards a 1.0 release. Things I would like to tackle before then:

  • Proper Scala.js support that actually works
  • Provide an alternative serialization codec, probably using circe
  • Replace the README with a microsite

I will still maintain the 0.10.x branch for as long as people need it (within reason).

Still TODO on this PR

  • Rewrite the readme

cb372 and others added 30 commits September 29, 2017 20:34
It wasn't running on most of the code in the core module.

Since this PR is probably going to touch every file in the codebase,
why not throw some reformatting into the mix?
It's sometimes convenient to just get a plain old value back instead
of wrapping it in any monad. This mode allows you to do this even with
async cache impls e.g. memcached.
It wasn't necessary to split the effect and the monad apart after all.
Put them back together again, thus removing a type param and a
natural transformation application from loads of places.
No need to parameterise CacheAlg and Mode on Sync/Async
Non-compiling tests are commented out for now
Because of changes in the API, we can now hardcode the representation to
Array[Byte]
@philwills
Copy link
Contributor

⛰ 🗻

I was just discussing what I'd like to do with Scanamo and @LATaylor-guardian pointed out you'd just done the equivalent here. I might take some notes.

@cb372
Copy link
Owner Author

cb372 commented Oct 27, 2017

@LATaylor-guardian stop stalking me!

@philwills happy to talk you through it tonight over a 🍺 at the ⭐️ of 👑

@cb372 cb372 changed the title The big one Bring Your Own Effect Oct 31, 2017
@ben-manes
Copy link

ben-manes commented Nov 1, 2017

I haven't developed in Scala in 5 or 6 years, so apologies that I cannot provide feedback as an active user. My main concerns are,

  1. It is difficult to have a power, but unified API
  2. Advanced features do not delegate to native support if available
  3. The key type as a String is restrictive and inefficient for local caches.
  4. The API is designed around remote caches, which is probably leaks how you use it

I think all of these can be remedied.

@cb372 cb372 merged commit 18988ce into master Nov 7, 2017
@cb372 cb372 deleted the the-big-one branch November 7, 2017 10:40
@fommil
Copy link

fommil commented Nov 8, 2017

the only comment I'd have from reading your very clear description is that Mode is a bit generic... really you want to use MonadError or something but that would tie you to scalaz or cats. How about ScEffect?

}
}

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cb372 what do you think of putting this in a micro library? with the monix, cats, scalaz implementation as module. This way I could reuse that in the scala guardian client library and @philwills could as well in scanamo.

This is the kind of code I don't want to copy/paste and maintain in multiple librairies.

@cb372 cb372 mentioned this pull request Jan 26, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants