Skip to content

Commit 4c778fb

Browse files
committed
Typeclass instances for Reader.
- Alternative - Defer - Monad - Monoid - Eq
1 parent fedfe4c commit 4c778fb

File tree

4 files changed

+115
-2
lines changed

4 files changed

+115
-2
lines changed
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
package io.catbird
22

3-
package object util extends FutureInstances with TryInstances with VarInstances with AsyncStreamInstances
3+
package object util
4+
extends AsyncStreamInstances
5+
with FutureInstances
6+
with ReaderInstances
7+
with TryInstances
8+
with VarInstances
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package io.catbird
2+
package util
3+
4+
import cats.{ Alternative, Defer, Eq, Monad, Monoid, MonoidK, StackSafeMonad }
5+
import cats.instances.list._
6+
import com.twitter.io.Reader
7+
import com.twitter.util.{ Await, Duration }
8+
import scala.collection.immutable.List
9+
10+
/** Typeclass instances for [[com.twitter.io.Reader]]
11+
*
12+
* Note that while (to the best of my knowledge) these instances are lawful, Reader itself is inherently
13+
* unsafe: it is based on mutable state and also requires manual cleanup to ensure resource safety. To use
14+
* it in pure functional code, side-effecting operations should be wrapped in an effect type such as
15+
* [[Rerunnable]], and cleanup should be ensured using something like [[cats.effect.Resource]].
16+
*
17+
* TODO: add facilities to make that easier to do.
18+
*/
19+
trait ReaderInstances {
20+
implicit final val readerInstance: Alternative[Reader] with Defer[Reader] with Monad[Reader] =
21+
new Alternative[Reader] with ReaderDefer with ReaderMonad with ReaderMonoidK
22+
23+
implicit final def readerMonoid[A](implicit A: Monoid[A]) = new ReaderMonoid[A]
24+
25+
/**
26+
* Obtain a [[cats.Eq]] instance for [[com.twitter.io.Reader]].
27+
*
28+
* These instances use [[com.twitter.util.Await]] so should be
29+
* [[https://finagle.github.io/blog/2016/09/01/block-party/ avoided in production code]]. Likely use cases
30+
* include tests, scrips, REPLs etc.
31+
*/
32+
final def readerEq[A](atMost: Duration)(implicit A: Eq[A]): Eq[Reader[A]] = new Eq[Reader[A]] {
33+
final override def eqv(x: Reader[A], y: Reader[A]) =
34+
Await.result(
35+
Reader.readAllItems(x).joinWith(Reader.readAllItems(y))((xs, ys) => Eq[List[A]].eqv(xs.toList, ys.toList)),
36+
atMost
37+
)
38+
}
39+
}
40+
41+
/** Monad instance for twitter-util's Reader streaming abstraction
42+
*
43+
* Also entrant for "most confusing class name of the year"
44+
*/
45+
private[util] trait ReaderMonad extends StackSafeMonad[Reader] {
46+
final override def map[A, B](fa: Reader[A])(f: A => B): Reader[B] = fa.map(f)
47+
final override def pure[A](a: A): Reader[A] = Reader.value(a)
48+
final override def flatMap[A, B](fa: Reader[A])(f: A => Reader[B]): Reader[B] = fa.flatMap(f)
49+
final override def flatten[A](ffa: Reader[Reader[A]]): Reader[A] = ffa.flatten
50+
}
51+
52+
private[util] trait ReaderDefer extends Defer[Reader] {
53+
/** Defer creation of a Reader
54+
*
55+
* There are a few ways to achieve this, such as using fromIterator on a lazy iterator, but this is the
56+
* simplest I've come up with. Might be worth benchmarking alternatives.
57+
*/
58+
def defer[A](fa: => Reader[A]): Reader[A] = Reader.flatten(Reader.value(() => fa).map(_()))
59+
}
60+
61+
private[util] trait ReaderMonoidK extends MonoidK[Reader] {
62+
final override def empty[A]: Reader[A] = Reader.empty
63+
final override def combineK[A](x: Reader[A], y: Reader[A]): Reader[A] = Reader.concat(List(x, y))
64+
}
65+
66+
private[util] final class ReaderMonoid[A](implicit A: Monoid[A]) extends Monoid[Reader[A]] {
67+
final override def empty = Reader.value(A.empty)
68+
final override def combine(xs: Reader[A], ys: Reader[A]) =
69+
for(x <- xs; y <- ys) yield A.combine(x, y)
70+
}

util/src/test/scala/io/catbird/util/arbitrary.scala

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package io.catbird.util
22

33
import com.twitter.concurrent.AsyncStream
44
import com.twitter.conversions.DurationOps._
5+
import com.twitter.io.Reader
56
import com.twitter.util.{ Future, Return, Try, Var }
6-
import org.scalacheck.{ Arbitrary, Cogen }
7+
import org.scalacheck.{ Arbitrary, Gen, Cogen }
78

89
trait ArbitraryInstances {
910
implicit def futureArbitrary[A](implicit A: Arbitrary[A]): Arbitrary[Future[A]] =
@@ -18,6 +19,22 @@ trait ArbitraryInstances {
1819
implicit def asyncStreamArbitrary[A](implicit A: Arbitrary[A]): Arbitrary[AsyncStream[A]] =
1920
Arbitrary(A.arbitrary.map(AsyncStream.of))
2021

22+
// Note that this doesn't cover BufReader or InputStreamReader currently
23+
private def readerGen[A: Arbitrary](depth: Int = 0): Gen[Reader[A]] = {
24+
val options = List(
25+
1 -> Gen.const(Reader.empty[A]), // empty reader defined inline
26+
3 -> Arbitrary.arbitrary[A].flatMap(Reader.value(_)), // FutureReader (used for both fromFuture and value)
27+
1 -> Gen.const(Reader.exception(new Exception)), // also FutureReader but making sure we exercise the failed case
28+
3 -> Gen.listOf(Arbitrary.arbitrary[A]).flatMap(Reader.fromSeq(_)), // IteratorReader
29+
2 -> Arbitrary.arbitrary[AsyncStream[A]].flatMap(Reader.fromAsyncStream(_)) // uses Pipe
30+
)
31+
lazy val flatten = 2 -> readerGen[A](depth + 1).map(Reader.value(_).flatten) // flatten any of the other types
32+
Gen.frequency((if (depth < 4) flatten :: options else options): _*)
33+
}
34+
35+
implicit def readerArbitrary[A: Arbitrary]: Arbitrary[Reader[A]] =
36+
Arbitrary(readerGen[A]())
37+
2138
implicit def rerunnableArbitrary[A](implicit A: Arbitrary[A]): Arbitrary[Rerunnable[A]] =
2239
Arbitrary(futureArbitrary[A].arbitrary.map(Rerunnable.fromFuture[A](_)))
2340

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.catbird
2+
package util
3+
4+
import cats.Eq
5+
import cats.instances.boolean._
6+
import cats.instances.int._
7+
import cats.instances.tuple._
8+
import cats.kernel.laws.discipline.MonoidTests
9+
import cats.laws.discipline.{ AlternativeTests, DeferTests, MonadTests }
10+
import com.twitter.io.Reader
11+
import com.twitter.conversions.DurationOps._
12+
13+
class ReaderSuite extends CatbirdSuite with ReaderInstances with ArbitraryInstances with EqInstances {
14+
implicit private def eqReader[A: Eq]: Eq[Reader[A]] = readerEq[A](1.second)
15+
16+
checkAll("Reader[Int]", AlternativeTests[Reader].alternative[Int, Int, Int])
17+
checkAll("Reader[Int]", DeferTests[Reader].defer[Int])
18+
checkAll("Reader[Int]", MonadTests[Reader].monad[Int, Int, Int])
19+
20+
checkAll("Reader[Int]", MonoidTests[Reader[Int]].monoid)
21+
}

0 commit comments

Comments
 (0)