Chapter 2: Connecting to Redis

In this chapter we start from the beginning. First we write a program that connects to a Redis database and returns a value, and then we run that program in the REPL. We also touch a composing small programs to construct larger ones.

Our First program

Before we can use brick we need to import some symbols. We will use package imports here as a convenience; this will give us the most commonly-used symbols when working with the API.

import eu.monniot.brick._

Let’s also bring in Cats.

import cats._
import cats.effect._
import cats.implicits._

In the brick API the most common types we will deal with have the form CommandIO[A], specifying computations that take place in a context where a Redis session is available, ultimately producing a value of type A.

So let’s start a CommandIO[A] program that simply returns a constant.

scala> val program1 = 42.pure[CommandIO]
program1: eu.monniot.brick.CommandIO[Int] = Free(...)

This is a perfectly respectable brick program, but we can’t run it as-is; we need a Redis session first. There are several ways to do this, but here let’s use a Transactor.

// A transactor that gets connections from io.lettuce.core.RedisClient
val xa = lettuce.Transactor.fromRedisClient("redis://password@localhost:6379/0")

A Transactor is a data type that knows how to connect to a Redis instance; and with this knowledge it can transform CommandIO ~> IO, which gives us a program we can run. Specifically it gives us an IO that, when run, will connect to the database and execute a single transaction.

We are using cats.effect.IO as our final effect type, but you can use any monad M[_] given cats.effect.Async[M]. See [Using Your Own Target Monad] at the end of this chapter for more details.

The Transactor we used simply delegates to the io.lettuce.core.StatefulRedisConnection<K, V> to allocate and manage connections.

And here we go.

scala> val io = program1.exec(xa)
io: cats.effect.IO[Int] = IO$1853102201

scala> io.unsafeRunSync()
res1: Int = 42

Hooray! We have computed a constant. It’s not very interesting because we never ask the database to perform any work, but it’s a first step.

Keep in mind that all the code in this documentation is pure except the calls to IO.unsafeRunSync, which is the “end of the world” operation that typically appears only at your application’s entry points. In the REPL we use it to force a computation to “happen”.

Right. Now let’s try something more interesting.

Our Second Program

Now let’s use the echo commands to construct a query that asks Redis to return a constant.

scala> val program2 = echo("42")
program2: eu.monniot.brick.CommandIO[String] = Free(...)

scala> val io2 = program2.exec(xa)
io2: cats.effect.IO[String]

scala> io2.unsafeRunSync()
res2: String = 42

Ok! We have now connected to a Redis instance to compute a constant. Considerably more impressive.

Our Third Program

What if we want to do more than one thing? Easy! CommandIO is a monad, so we can use a for comprehension to compose two smaller programs into one larger program.

val program3a: CommandIO[String] = for {
    a <- get("myCompositeKey")
    b = a.getOrElse("a:b:c").split(":").mkString(";")
    c <- set("myCompositeKey", b)
} yield c

And Io, it was good:

scala> program3a.exec(xa).unsafeRunSync
res4: String = a;b;c

Diving Deeper

All of the brick monads are implemented via cats.free.Free and have no operational semantics; we can only “run” a brick program by transforming CommandIO to a monad that actually has some meaning.

Out of the box brick doesn’t provides any interpreter, as they need to rely on an actual Redis Connection. It does provides the skeleton to build an interpreter from its free monads to Kleisli[M, CommandIO, ?] given Async[M].

scala> import cats.~>
import cats.$tilde$greater

scala> import cats.data.Kleisli
import cats.data.Kleisli

scala> import eu.monniot.brick.free.command.CommandOp
import eu.monniot.brick.free.command.CommandOp

scala> val interpreter = new interpreters.KleisliInterpreter[IO] {
    val KeysInterpreter = null
    val StringsInterpreter = null
    ...
}
// TODO Return type

scala> val kleisli = program1.foldMap(interpreter)

scala> val io = IO(null: ???) >>= kleisli.run
io: cats.effect.IO[Int] = IO$2938495283

scala> io.unsafeRunSync() // sneaky; program1 never looks at the connection
res6: Int = 42

So the interpreter above is used to transform a CommandIO[A] program into a Kleisli[IO, Conn, A]. Then we construct an IO[Conn] (returning null) and bind it through the Kleisli, yielding our IO[Int]. This of course only works because program1 is a pure value that does not look at the connection.

The Transactor that we defined at the beginning of this chapter is basically a utility that allows us to do the same as above using program1.exec(xa).

Using Your Own Target Monad

As mentionned earlier, you can any monad M[_] given cats.effect.Async[M]. For example, here we use [Monix] Task.

import monix.eval.Task

val mxa = Transactor.fromRedisClient[Task]("redis://localhost:6379/0")

val task: Task[String] = get("key").exec