This weekend I finally managed to close the last open issue for the first milestone of Carlos, “Open source day”! The project is now available on Github with a MIT license, so feel free to do whatever you like with it.

Disclaimer: We currently use it in production code.

Table of questions:

What is Carlos?

From the README:

Carlos is a small set of classes, global functions (that will be replaced by protocol extensions with Swift 2.0) and convenience operators to realize custom, flexible and powerful cache layers in your application.

By default, Carlos ships with an in-memory cache, a disk cache and a simple network fetcher.

In other words, Carlos is a way for you to implement cache infrastructures that can be either very simple (like the memory-disk-network built-in infrastructure) or very complex (with requests pooling, condition-based enabled levels, requests cap on some or all of the levels, custom keys, custom and different values stored by every level, etc.)

What problem it solves?

In WeltN24, we needed to integrate a cache layer for images in our app. Namely, our cache needed to have a memory layer so that it was fast to access images after they were first loaded, a disk layer so that the images were cached between different usages of the app (cache invalidation was not a big issue, luckily), and a network fetcher to fetch the images from the network for the first time the user accesses them.

Up to this point, Haneke worked really great for us. It was painless to integrate and it did its job very well.

At some point we needed some more customization though. Since we let the users decide whether they want to download all the images of a given issue together with the issue itself, we wanted in some way to pre-populate the cache, without actually having all the images in memory. Also, we didn’t want to manually save the images on the folder that Haneke uses for the disk cache, since this meant going too deep with the implementation details of Haneke, making the code too fragile.

What we wanted was basically to have a new intermediate layer between the disk cache and the network fetcher. It seems to us that this is not possible with Haneke at the moment.

So we decided to roll out our own cache, and since we were at it, we decided to make it awesome :)

How does it solve the problem?

The main idea behind Carlos is to apply the mechanism of function composition, where you can have several functions and chain them together (maybe with a custom operator to make everything nicer), to whole cache layers, that asynchronously fetch values.

What the final result looks like:

let cache = MemoryCacheLevel<NSData>() >>> DiskCacheLevel() >>> ({ NSURL(string: $0)! } =>> NetworkFetcher())

cache.get("").onSuccess({ value in
    println("I found \(value)!")
}).onFailure({ error in
    println("An error occurred :( \(error)")

The >>> operator is the composition operator. We have a bunch of other operators (for value and key transformation: =>>, as you can see in the example above, or for conditioned levels: <?>), but we always have functions as well, in case one doesn’t want to mess with operators (even though it’s more readable). We plan to move all the functions we currently have to protocol extensions as soon as Swift 2.0 comes out, though.

If you’re curious, much more complex examples are available on the repo README.

What is the future development?

We have a lot of ideas in mind for future versions of Carlos. Some of them will break its API, since the project is so young and there is no concept of stable API yet. But they will also make the library even better, easier to use, and more powerful at the same time.

If you’re curious, head over here.

The biggest idea we’re currently designing is the abstraction of cache levels and key/value transformers. We would like them to work the same way, so that one can not only compose cache layers, but actually chain them too.

How can I contribute?

Contributions are welcome! We’re in the early stage of development so your contributions could shape the design of Carlos.

If you want to contribute to the project, please:

  • Create an issue explaining your problem and your solution
  • Create a branch with the issue number and a short abstract of the feature name
  • Implement your solution
  • Write tests (untested features won’t be merged)
  • When all the tests are written and green, create a pull request, with a short description of the approach taken

Thanks for reading! I’ll probably write another post about advanced usages soon :)