diff --git a/modules/core/src/main/scala/dev/profunktor/redis4cats/hlist.scala b/modules/core/src/main/scala/dev/profunktor/redis4cats/hlist.scala index af8a9a9d..ef6a12c9 100644 --- a/modules/core/src/main/scala/dev/profunktor/redis4cats/hlist.scala +++ b/modules/core/src/main/scala/dev/profunktor/redis4cats/hlist.scala @@ -28,11 +28,34 @@ object hlist { sealed trait HList { def ::[A](a: A): HCons[A, this.type] = HCons(a, this) + + def reverse: HList = { + def go(ys: HList, res: HList): HList = + ys match { + case HNil => res + case HCons(h, t) => go(t, h :: res) + } + go(this, HNil) + } } final case class HCons[+H, +Tail <: HList](head: H, tail: Tail) extends HList case object HNil extends HList + object HList { + implicit class HListOps[T <: HList](t: T) { + def filterUnit[R <: HList](implicit w: Filter.Aux[T, R]): R = { + def go(ys: HList, res: HList): HList = + ys match { + case HNil => res + case HCons(h, t) if h == () => go(t, res) + case HCons(h, t) => go(t, h :: res) + } + go(t, HNil).reverse.asInstanceOf[w.R] + } + } + } + object ~: { def unapply[H, T <: HList](l: H :: T): Some[(H, T)] = Some((l.head, l.tail)) } @@ -67,4 +90,40 @@ object hlist { new Witness[HCons[F[A], T]] { type R = HCons[A, w.R] } } + /* + * It represents a relationship between a raw list and a + * filtered one. Mainly used to filter out values of type Unit. + */ + sealed trait Filter[T <: HList] { + type R <: HList + } + + object Filter { + type Aux[T0 <: HList, R0 <: HList] = Filter[T0] { type R = R0 } + + implicit val hnil: Filter.Aux[HNil, HNil] = + new Filter[HNil] { type R = HNil } + + implicit def hconsUnit[T <: HList](implicit w: Filter[T]): Filter.Aux[HCons[Unit, T], w.R] = + new Filter[HCons[Unit, T]] { type R = w.R } + + implicit def hconsNotUnit[A: =!=[Unit, *], T <: HList](implicit w: Filter[T]): Filter.Aux[HCons[A, T], A :: w.R] = + new Filter[HCons[A, T]] { type R = A :: w.R } + } + + /** + * Type inequality + * + * Credits: https://stackoverflow.com/a/6929051 + */ + sealed class =!=[A, B] + + object =!= extends NEqualLowPriority { + implicit def nequal[A, B]: =!=[A, B] = new =!=[A, B] + } + + trait NEqualLowPriority { + implicit def equal[A]: =!=[A, A] = sys.error("should not be called") + } + } diff --git a/modules/core/src/test/scala/dev/profunktor/redis4cats/HListSpec.scala b/modules/core/src/test/scala/dev/profunktor/redis4cats/HListSpec.scala index 4f91669f..cdabcca0 100644 --- a/modules/core/src/test/scala/dev/profunktor/redis4cats/HListSpec.scala +++ b/modules/core/src/test/scala/dev/profunktor/redis4cats/HListSpec.scala @@ -47,4 +47,16 @@ class HListSpec extends AnyFunSuite with Matchers { assert(n2.isInstanceOf[Int]) } + test("Filter out values") { + val unit = () + val hl = unit :: "hi" :: 33 :: unit :: false :: 's' :: unit :: HNil + + val s ~: n ~: b ~: c ~: HNil = hl.filterUnit + + assert(s.isInstanceOf[String]) + assert(n.isInstanceOf[Int]) + assert(b.isInstanceOf[Boolean]) + assert(c.isInstanceOf[Char]) + } + } diff --git a/modules/effects/src/main/scala/dev/profunktor/redis4cats/pipeline.scala b/modules/effects/src/main/scala/dev/profunktor/redis4cats/pipeline.scala index f86ee4ec..688908f9 100644 --- a/modules/effects/src/main/scala/dev/profunktor/redis4cats/pipeline.scala +++ b/modules/effects/src/main/scala/dev/profunktor/redis4cats/pipeline.scala @@ -17,6 +17,7 @@ package dev.profunktor.redis4cats import cats.effect._ +import cats.syntax.functor._ import dev.profunktor.redis4cats.effect.Log import dev.profunktor.redis4cats.hlist._ import scala.util.control.NoStackTrace @@ -29,6 +30,23 @@ object pipeline { cmd: RedisCommands[F, K, V] ) { + /** + * Same as @exec, except it filters out values of type Unit + * from its result. + */ + def exec_[T <: HList, R <: HList, S <: HList](commands: T)( + implicit w: Witness.Aux[T, R], + f: Filter.Aux[R, S] + ): F[S] = exec[T, R](commands).map(_.filterUnit) + + /*** + * Exclusively run Redis commands as part of a pipeline (autoflush: disabled). + * + * Once all the commands have been executed, @exec will "flush" them into Redis, + * and finally re-enable autoflush. + * + * @return `F[R]` or raises a @PipelineError in case of failure. + */ def exec[T <: HList, R <: HList](commands: T)(implicit w: Witness.Aux[T, R]): F[R] = Runner[F].exec( Runner.Ops( diff --git a/modules/effects/src/main/scala/dev/profunktor/redis4cats/transactions.scala b/modules/effects/src/main/scala/dev/profunktor/redis4cats/transactions.scala index 22e04128..2ea8e80a 100644 --- a/modules/effects/src/main/scala/dev/profunktor/redis4cats/transactions.scala +++ b/modules/effects/src/main/scala/dev/profunktor/redis4cats/transactions.scala @@ -32,6 +32,15 @@ object transactions { cmd: RedisCommands[F, K, V] ) { + /** + * Same as @exec, except it filters out values of type Unit + * from its result. + */ + def exec_[T <: HList, R <: HList, S <: HList](commands: T)( + implicit w: Witness.Aux[T, R], + f: Filter.Aux[R, S] + ): F[S] = exec[T, R](commands).map(_.filterUnit) + /*** * Exclusively run Redis commands as part of a transaction. * @@ -41,6 +50,8 @@ object transactions { * * It should not be used to run other computations, only Redis commands. Fail to do so * may end in unexpected results such as a dead lock. + * + * @return `F[R]` or it raises a @TransactionError in case of failure. */ def exec[T <: HList, R <: HList](commands: T)(implicit w: Witness.Aux[T, R]): F[R] = Runner[F].exec( diff --git a/modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisPipelineDemo.scala b/modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisPipelineDemo.scala index 47622886..3dbacbcf 100644 --- a/modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisPipelineDemo.scala +++ b/modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisPipelineDemo.scala @@ -54,9 +54,9 @@ object RedisPipelineDemo extends LoggerIOApp { val prog = RedisPipeline(cmd) - .exec(operations) + .exec_(operations) .flatMap { - case _ ~: _ ~: res1 ~: _ ~: _ ~: res2 ~: HNil => + case res1 ~: res2 ~: HNil => putStrLn(s"res1: $res1, res2: $res2") } .onError { diff --git a/site/docs/pipelining.md b/site/docs/pipelining.md index 3c56105f..8a7bf0bf 100644 --- a/site/docs/pipelining.md +++ b/site/docs/pipelining.md @@ -17,7 +17,7 @@ Use [pipelining](https://redis.io/topics/pipelining) to speed up your queries by ### RedisPipeline usage -The API for disabling / enabling autoflush and flush commands manually is available for you to use but since the pattern is so common it is recommended to just use `RedisPipeline`. You can create a pipeline by passing the commands API as a parameter and invoke the `exec` function given the set of commands you wish to send to the server. +The API for disabling / enabling autoflush and flush commands manually is available for you to use but since the pattern is so common it is recommended to just use `RedisPipeline`. You can create a pipeline by passing the commands API as a parameter and invoke the `exec` function (or `exec_`) given the set of commands you wish to send to the server. Note that every command has to be forked (`.start`) because the commands need to be sent to the server in an asynchronous way but no response will be received until the commands are successfully flushed. Also, it is not possible to sequence commands (`flatMap`) that are part of a pipeline. Every command has to be atomic and independent of previous results. @@ -68,9 +68,9 @@ commandsApi.use { cmd => // RedisCommands[IO, String, String] val prog = RedisPipeline(cmd) - .exec(operations) + .exec_(operations) .flatMap { - case _ ~: _ ~: res1 ~: _ ~: _ ~: res2 ~: HNil => + case res1 ~: res2 ~: HNil => putStrLn(s"res1: $res1, res2: $res2") } .onError { @@ -84,3 +84,5 @@ commandsApi.use { cmd => // RedisCommands[IO, String, String] } ``` +The `exec_` function filters out values of type `Unit`, which are normally irrelevant. If you find yourself needing the `Unit` types to verify some behavior, use `exec` instead. + diff --git a/site/docs/transactions.md b/site/docs/transactions.md index d81cbf12..664b0f7d 100644 --- a/site/docs/transactions.md +++ b/site/docs/transactions.md @@ -16,7 +16,7 @@ and handle the possible errors and retry logic. ### Working with transactions -The most common way is to create a `RedisTransaction` once by passing the commands API as a parameter and invoke the `exec` function every time you want to run the given commands as part of a new transaction. +The most common way is to create a `RedisTransaction` once by passing the commands API as a parameter and invoke the `exec` function (or `exec_`) every time you want to run the given commands as part of a new transaction. Every command has to be atomic and independent of previous Redis results, so it is not recommended to chain commands using `flatMap`. @@ -70,9 +70,9 @@ commandsApi.use { cmd => // RedisCommands[IO, String, String] // the result type is inferred as well // Unit :: Option[String] :: Unit :: HNil val prog = - tx.exec(commands) + tx.exec_(commands) .flatMap { - case _ ~: res1 ~: _ ~: HNil => + case res1 ~: HNil => putStrLn(s"Key1 result: $res1") } .onError { @@ -96,6 +96,8 @@ Transactional commands may be discarded if something went wrong in between. The - `TransactionAborted`: The `DISCARD` command was triggered due to cancellation or other failure within the transaction. - `TimeoutException`: The transaction timed out due to some unknown error. +The `exec_` function filters out values of type `Unit`, which are normally irrelevant. If you find yourself needing the `Unit` types to verify some behavior, use `exec` instead. + ### How NOT to use transactions For example, the following transaction will result in a dead-lock: