Skip to content

Commit

Permalink
Merge pull request #278 from profunktor/feature/hlist-filter-unit-types
Browse files Browse the repository at this point in the history
Filter Unit types from HList
  • Loading branch information
gvolpe authored May 9, 2020
2 parents 99e03f4 + 0d0d3af commit df85762
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 8 deletions.
59 changes: 59 additions & 0 deletions modules/core/src/main/scala/dev/profunktor/redis4cats/hlist.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.isInstanceOf[Unit] => 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))
}
Expand Down Expand Up @@ -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")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 5 additions & 3 deletions site/docs/pipelining.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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 {
Expand All @@ -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.

8 changes: 5 additions & 3 deletions site/docs/transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down Expand Up @@ -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 {
Expand All @@ -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:
Expand Down

0 comments on commit df85762

Please sign in to comment.