Skip to content

Commit

Permalink
Add more Parallel instances (#1938)
Browse files Browse the repository at this point in the history
* Initial version of Parallel

* Add Either/Validated Parallel instance

* Break up syntax for parallel

* Add Parallel syntax tests

* Add Tuple syntax for Parallel

* Add ParallelTests

* Fix Parallel law

* Add more tests

* Add Parallel Kleisli instance

* Add instances for OptionT and EitherT to nested

* Add law tests for parallel OptionT and EitherT

* Make EitherT instance forward to Validated

* Add Kleisli lawTest

* Add WriterT instance

* Add more tests

* Add scaladoc

* Add ApplicativeError instance for MonadError and Parallel

* Add Test that actually hits the implicated ApplicativeError instance

* Fix mixup

* Move appError instance to Parallel companion object

* Fix apperror test

* Add sequential roundtrip law

* Add ZipNEL and ZipNEV and Parallel instances

* Add law for testing that pure is consistent across Parallel pairs

* Add Parallel Serializable tests

* Add EitherT Parallel instance that doesn't require a Parallel Instance for M

* Add ZipVector + Parallel instance

* Add ZipVector test

* Add scaladoc to ApplicativeError and change order of type parameters

* Add Parallel#identity function

* Add identity instances for Future and Id

* Simplify parAp2 implementation

* Refactor Parallel

* Add applicativeError instace method

* Reverse applicativeError and remove redundant .apply

* Simplify further

* Add FailFastFuture + Parallel instance

* Shorten wait times

* Add ZipStream and OneAnd instance

* Convert traits to abstract classes

* Add consistency test for zip stream

* Add Applicative test for Applicative[OneAnd]

* Add parAp test

* Add ZipList and lawtest all Zip* instances

* Add ZipList consistency test

* Add NonEmptyParallel

* Add test cases for ParNonEmptyTraverse

* Update scaladoc

* Remove FailFastFuture and all Zip* instances

* Rename methods in NonEmptyParallel

* optimize AppError

* Add parFlatTraverse and sequence

* Revert "Remove FailFastFuture and all Zip* instances"

This reverts commit c5e3423.

* Add parFlatTraverse and sequence

* Add isomorphic functor law

* Add ZipMap?

* Fix law test parameters

* Remove ZipMap

* Fix the priority of OneAnd instances

* Rename Parallel tests

* Rename Parallel tests

* Add mima exceptions

* Remove fail fast future
  • Loading branch information
LukaJCB authored and kailuowang committed Nov 18, 2017
1 parent c5f76a2 commit ef64ff8
Show file tree
Hide file tree
Showing 18 changed files with 357 additions and 27 deletions.
9 changes: 9 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,15 @@ def mimaSettings(moduleName: String) = Seq(
import com.typesafe.tools.mima.core.ProblemFilters._
Seq(
exclude[ReversedMissingMethodProblem]("cats.syntax.FoldableSyntax.catsSyntaxFoldOps"),
exclude[MissingTypesProblem]("cats.data.OneAndLowPriority3"),
exclude[MissingTypesProblem]("cats.data.OneAndLowPriority2"),
exclude[MissingTypesProblem]("cats.data.OneAndLowPriority1"),
exclude[DirectMissingMethodProblem]("cats.data.OneAndLowPriority3.catsDataNonEmptyTraverseForOneAnd"),
exclude[DirectMissingMethodProblem]("cats.data.OneAndLowPriority2.catsDataTraverseForOneAnd"),
exclude[ReversedMissingMethodProblem]("cats.instances.ParallelInstances.catsStdNonEmptyParallelForZipVector"),
exclude[ReversedMissingMethodProblem]("cats.instances.ParallelInstances.catsStdParallelForZipStream"),
exclude[ReversedMissingMethodProblem]("cats.instances.ParallelInstances.catsStdNonEmptyParallelForZipList"),
exclude[ReversedMissingMethodProblem]("cats.instances.ParallelInstances.catsStdParallelForFailFastFuture"),
exclude[DirectMissingMethodProblem]("cats.data.EitherTInstances2.catsDataMonadErrorForEitherT")
)
}
Expand Down
36 changes: 36 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyList.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cats
package data

import cats.data.NonEmptyList.ZipNonEmptyList
import cats.instances.list._
import cats.syntax.order._
import scala.annotation.tailrec
Expand Down Expand Up @@ -421,6 +422,27 @@ object NonEmptyList extends NonEmptyListInstances {
def fromReducible[F[_], A](fa: F[A])(implicit F: Reducible[F]): NonEmptyList[A] =
F.toNonEmptyList(fa)

class ZipNonEmptyList[A](val value: NonEmptyList[A]) extends AnyVal

object ZipNonEmptyList {

def apply[A](nev: NonEmptyList[A]): ZipNonEmptyList[A] =
new ZipNonEmptyList(nev)

implicit val catsDataCommutativeApplyForZipNonEmptyList: CommutativeApply[ZipNonEmptyList] =
new CommutativeApply[ZipNonEmptyList] {
def ap[A, B](ff: ZipNonEmptyList[A => B])(fa: ZipNonEmptyList[A]): ZipNonEmptyList[B] =
ZipNonEmptyList(ff.value.zipWith(fa.value)(_ apply _))

override def map[A, B](fa: ZipNonEmptyList[A])(f: (A) => B): ZipNonEmptyList[B] =
ZipNonEmptyList(fa.value.map(f))

override def product[A, B](fa: ZipNonEmptyList[A], fb: ZipNonEmptyList[B]): ZipNonEmptyList[(A, B)] =
ZipNonEmptyList(fa.value.zipWith(fb.value){ case (a, b) => (a, b) })
}

implicit def zipNelEq[A: Eq]: Eq[ZipNonEmptyList[A]] = Eq.by(_.value)
}
}

private[data] sealed abstract class NonEmptyListInstances extends NonEmptyListInstances0 {
Expand Down Expand Up @@ -537,6 +559,20 @@ private[data] sealed abstract class NonEmptyListInstances extends NonEmptyListIn
new NonEmptyListOrder[A] {
val A0 = A
}

implicit def catsDataNonEmptyParallelForNonEmptyList[A]: NonEmptyParallel[NonEmptyList, ZipNonEmptyList] =
new NonEmptyParallel[NonEmptyList, ZipNonEmptyList] {

def flatMap: FlatMap[NonEmptyList] = NonEmptyList.catsDataInstancesForNonEmptyList

def apply: Apply[ZipNonEmptyList] = ZipNonEmptyList.catsDataCommutativeApplyForZipNonEmptyList

def sequential: ZipNonEmptyList ~> NonEmptyList =
λ[ZipNonEmptyList ~> NonEmptyList](_.value)

def parallel: NonEmptyList ~> ZipNonEmptyList =
λ[NonEmptyList ~> ZipNonEmptyList](nel => new ZipNonEmptyList(nel))
}
}

private[data] sealed abstract class NonEmptyListInstances0 extends NonEmptyListInstances1 {
Expand Down
33 changes: 33 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyVector.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cats
package data

import cats.data.NonEmptyVector.ZipNonEmptyVector
import scala.annotation.tailrec
import scala.collection.immutable.{TreeSet, VectorBuilder}
import cats.instances.vector._
Expand Down Expand Up @@ -353,6 +354,18 @@ private[data] sealed abstract class NonEmptyVectorInstances {
implicit def catsDataSemigroupForNonEmptyVector[A]: Semigroup[NonEmptyVector[A]] =
catsDataInstancesForNonEmptyVector.algebra

implicit def catsDataParallelForNonEmptyVector[A]: NonEmptyParallel[NonEmptyVector, ZipNonEmptyVector] =
new NonEmptyParallel[NonEmptyVector, ZipNonEmptyVector] {

def apply: Apply[ZipNonEmptyVector] = ZipNonEmptyVector.catsDataCommutativeApplyForZipNonEmptyVector
def flatMap: FlatMap[NonEmptyVector] = NonEmptyVector.catsDataInstancesForNonEmptyVector

def sequential: ZipNonEmptyVector ~> NonEmptyVector =
λ[ZipNonEmptyVector ~> NonEmptyVector](_.value)

def parallel: NonEmptyVector ~> ZipNonEmptyVector =
λ[NonEmptyVector ~> ZipNonEmptyVector](nev => new ZipNonEmptyVector(nev))
}

}

Expand All @@ -379,5 +392,25 @@ object NonEmptyVector extends NonEmptyVectorInstances with Serializable {
if (vector.nonEmpty) new NonEmptyVector(vector)
else throw new IllegalArgumentException("Cannot create NonEmptyVector from empty vector")

class ZipNonEmptyVector[A](val value: NonEmptyVector[A]) extends Serializable

object ZipNonEmptyVector {

def apply[A](nev: NonEmptyVector[A]): ZipNonEmptyVector[A] =
new ZipNonEmptyVector(nev)

implicit val catsDataCommutativeApplyForZipNonEmptyVector: CommutativeApply[ZipNonEmptyVector] =
new CommutativeApply[ZipNonEmptyVector] {
def ap[A, B](ff: ZipNonEmptyVector[A => B])(fa: ZipNonEmptyVector[A]): ZipNonEmptyVector[B] =
ZipNonEmptyVector(ff.value.zipWith(fa.value)(_ apply _))

override def map[A, B](fa: ZipNonEmptyVector[A])(f: (A) => B): ZipNonEmptyVector[B] =
ZipNonEmptyVector(fa.value.map(f))

override def product[A, B](fa: ZipNonEmptyVector[A], fb: ZipNonEmptyVector[B]): ZipNonEmptyVector[(A, B)] =
ZipNonEmptyVector(fa.value.zipWith(fb.value){ case (a, b) => (a, b) })
}

implicit def zipNevEq[A: Eq]: Eq[ZipNonEmptyVector[A]] = Eq.by(_.value)
}
}
45 changes: 39 additions & 6 deletions core/src/main/scala/cats/data/OneAnd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,22 @@ final case class OneAnd[F[_], A](head: A, tail: F[A]) {
}


private[data] sealed abstract class OneAndInstances extends OneAndLowPriority3 {
private[data] sealed abstract class OneAndInstances extends OneAndLowPriority0 {

implicit def catsDataParallelForOneAnd[A, M[_] : Alternative, F[_] : Alternative]
(implicit P: Parallel[M, F]): Parallel[OneAnd[M, ?], OneAnd[F, ?]] =
new Parallel[OneAnd[M, ?], OneAnd[F, ?]] {
def monad: Monad[OneAnd[M, ?]] = catsDataMonadForOneAnd(P.monad, Alternative[M])

def applicative: Applicative[OneAnd[F, ?]] = catsDataApplicativeForOneAnd(Alternative[F])

def sequential: OneAnd[F, ?] ~> OneAnd[M, ?] =
λ[OneAnd[F, ?] ~> OneAnd[M, ?]](ofa => OneAnd(ofa.head, P.sequential(ofa.tail)))

def parallel: OneAnd[M, ?] ~> OneAnd[F, ?] =
λ[OneAnd[M, ?] ~> OneAnd[F, ?]](ofa => OneAnd(ofa.head, P.parallel(ofa.tail)))

}

implicit def catsDataEqForOneAnd[A, F[_]](implicit A: Eq[A], FA: Eq[F[A]]): Eq[OneAnd[F, A]] =
new Eq[OneAnd[F, A]]{
Expand Down Expand Up @@ -186,7 +200,7 @@ private[data] sealed abstract class OneAndInstances extends OneAndLowPriority3 {
}
}

private[data] sealed abstract class OneAndLowPriority0 {
private[data] sealed abstract class OneAndLowPriority4 {
implicit val catsDataComonadForNonEmptyStream: Comonad[OneAnd[Stream, ?]] =
new Comonad[OneAnd[Stream, ?]] {
def coflatMap[A, B](fa: OneAnd[Stream, A])(f: OneAnd[Stream, A] => B): OneAnd[Stream, B] = {
Expand All @@ -207,8 +221,7 @@ private[data] sealed abstract class OneAndLowPriority0 {
}
}


private[data] sealed abstract class OneAndLowPriority1 extends OneAndLowPriority0 {
private[data] sealed abstract class OneAndLowPriority3 extends OneAndLowPriority4 {

implicit def catsDataFunctorForOneAnd[F[_]](implicit F: Functor[F]): Functor[OneAnd[F, ?]] =
new Functor[OneAnd[F, ?]] {
Expand All @@ -218,7 +231,27 @@ private[data] sealed abstract class OneAndLowPriority1 extends OneAndLowPriority

}

private[data] sealed abstract class OneAndLowPriority2 extends OneAndLowPriority1 {
private[data] sealed abstract class OneAndLowPriority2 extends OneAndLowPriority3 {

implicit def catsDataApplicativeForOneAnd[F[_]](implicit F: Alternative[F]): Applicative[OneAnd[F, ?]] =
new Applicative[OneAnd[F, ?]] {
override def map[A, B](fa: OneAnd[F, A])(f: A => B): OneAnd[F, B] =
fa.map(f)

def pure[A](x: A): OneAnd[F, A] =
OneAnd(x, F.empty)

override def ap[A, B](ff: OneAnd[F, A => B])(fa: OneAnd[F, A]): OneAnd[F, B] = {
val (f, tf) = (ff.head, ff.tail)
val (a, ta) = (fa.head, fa.tail)
val fb = F.ap(tf)(F.combineK(F.pure(a), ta))
OneAnd(f(a), F.combineK(F.map(ta)(f), fb))
}
}

}

private[data] sealed abstract class OneAndLowPriority1 extends OneAndLowPriority2 {

implicit def catsDataTraverseForOneAnd[F[_]](implicit F: Traverse[F]): Traverse[OneAnd[F, ?]] =
new Traverse[OneAnd[F, ?]] {
Expand All @@ -237,7 +270,7 @@ private[data] sealed abstract class OneAndLowPriority2 extends OneAndLowPriority
}


private[data] sealed abstract class OneAndLowPriority3 extends OneAndLowPriority2 {
private[data] sealed abstract class OneAndLowPriority0 extends OneAndLowPriority1 {

implicit def catsDataNonEmptyTraverseForOneAnd[F[_]](implicit F: Traverse[F], F2: Alternative[F]): NonEmptyTraverse[OneAnd[F, ?]] =
new NonEmptyReducible[OneAnd[F, ?], F] with NonEmptyTraverse[OneAnd[F, ?]] {
Expand Down
26 changes: 26 additions & 0 deletions core/src/main/scala/cats/data/ZipList.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package cats.data

import cats.{CommutativeApply, Eq}
import cats.instances.list.catsKernelStdEqForList

class ZipList[A](val value: List[A]) extends AnyVal

object ZipList {

def apply[A](value: List[A]): ZipList[A] = new ZipList(value)

implicit val catsDataCommutativeApplyForZipList: CommutativeApply[ZipList] = new CommutativeApply[ZipList] {

override def map[A, B](fa: ZipList[A])(f: (A) => B): ZipList[B] =
ZipList(fa.value.map(f))

def ap[A, B](ff: ZipList[A => B])(fa: ZipList[A]): ZipList[B] =
ZipList((ff.value, fa.value).zipped.map(_ apply _))

override def product[A, B](fa: ZipList[A], fb: ZipList[B]): ZipList[(A, B)] =
ZipList(fa.value.zip(fb.value))

}

implicit def catsDataEqForZipList[A: Eq]: Eq[ZipList[A]] = Eq.by(_.value)
}
32 changes: 32 additions & 0 deletions core/src/main/scala/cats/data/ZipStream.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package cats.data

import cats.{Alternative, CommutativeApplicative, Eq}
import cats.instances.stream._

class ZipStream[A](val value: Stream[A]) extends AnyVal

object ZipStream {

def apply[A](value: Stream[A]): ZipStream[A] = new ZipStream(value)

implicit val catsDataAlternativeForZipStream: Alternative[ZipStream] with CommutativeApplicative[ZipStream] =
new Alternative[ZipStream] with CommutativeApplicative[ZipStream] {
def pure[A](x: A): ZipStream[A] = new ZipStream(Stream.continually(x))

override def map[A, B](fa: ZipStream[A])(f: (A) => B): ZipStream[B] =
ZipStream(fa.value.map(f))

def ap[A, B](ff: ZipStream[A => B])(fa: ZipStream[A]): ZipStream[B] =
ZipStream((ff.value, fa.value).zipped.map(_ apply _))

override def product[A, B](fa: ZipStream[A], fb: ZipStream[B]): ZipStream[(A, B)] =
ZipStream(fa.value.zip(fb.value))

def empty[A]: ZipStream[A] = ZipStream(Stream.empty[A])

def combineK[A](x: ZipStream[A], y: ZipStream[A]): ZipStream[A] =
ZipStream(Alternative[Stream].combineK(x.value, y.value))
}

implicit def catsDataEqForZipStream[A: Eq]: Eq[ZipStream[A]] = Eq.by(_.value)
}
22 changes: 22 additions & 0 deletions core/src/main/scala/cats/data/ZipVector.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package cats.data

import cats.{CommutativeApply, Eq}
import cats.instances.vector._

class ZipVector[A](val value: Vector[A]) extends AnyVal

object ZipVector {

def apply[A](value: Vector[A]): ZipVector[A] = new ZipVector(value)

implicit val catsDataCommutativeApplyForZipVector: CommutativeApply[ZipVector] = new CommutativeApply[ZipVector] {

override def map[A, B](fa: ZipVector[A])(f: (A) => B): ZipVector[B] =
ZipVector(fa.value.map(f))
def ap[A, B](ff: ZipVector[A => B])(fa: ZipVector[A]): ZipVector[B] =
ZipVector((ff.value, fa.value).zipped.map(_ apply _))

}

implicit def catsDataEqForZipVector[A: Eq]: Eq[ZipVector[A]] = Eq.by(_.value)
}
42 changes: 41 additions & 1 deletion core/src/main/scala/cats/instances/parallel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package cats.instances
import cats.data._
import cats.kernel.Semigroup
import cats.syntax.either._
import cats.{Applicative, Functor, Monad, Parallel, ~>}
import cats.{Applicative, Apply, FlatMap, Functor, Monad, NonEmptyParallel, Parallel, ~>}


trait ParallelInstances extends ParallelInstances1 {
implicit def catsParallelForEitherValidated[E: Semigroup]: Parallel[Either[E, ?], Validated[E, ?]] = new Parallel[Either[E, ?], Validated[E, ?]] {
Expand Down Expand Up @@ -36,6 +37,45 @@ trait ParallelInstances extends ParallelInstances1 {
λ[OptionT[M, ?] ~> Nested[F, Option, ?]](optT => Nested(P.parallel(optT.value)))
}

implicit def catsStdNonEmptyParallelForZipList[A]: NonEmptyParallel[List, ZipList] =
new NonEmptyParallel[List, ZipList] {

def flatMap: FlatMap[List] = cats.instances.list.catsStdInstancesForList
def apply: Apply[ZipList] = ZipList.catsDataCommutativeApplyForZipList

def sequential: ZipList ~> List =
λ[ZipList ~> List](_.value)

def parallel: List ~> ZipList =
λ[List ~> ZipList](v => new ZipList(v))
}

implicit def catsStdNonEmptyParallelForZipVector[A]: NonEmptyParallel[Vector, ZipVector] =
new NonEmptyParallel[Vector, ZipVector] {

def flatMap: FlatMap[Vector] = cats.instances.vector.catsStdInstancesForVector
def apply: Apply[ZipVector] = ZipVector.catsDataCommutativeApplyForZipVector

def sequential: ZipVector ~> Vector =
λ[ZipVector ~> Vector](_.value)

def parallel: Vector ~> ZipVector =
λ[Vector ~> ZipVector](v => new ZipVector(v))
}

implicit def catsStdParallelForZipStream[A]: Parallel[Stream, ZipStream] =
new Parallel[Stream, ZipStream] {

def monad: Monad[Stream] = cats.instances.stream.catsStdInstancesForStream
def applicative: Applicative[ZipStream] = ZipStream.catsDataAlternativeForZipStream

def sequential: ZipStream ~> Stream =
λ[ZipStream ~> Stream](_.value)

def parallel: Stream ~> ZipStream =
λ[Stream ~> ZipStream](v => new ZipStream(v))
}


implicit def catsParallelForEitherTNestedParallelValidated[F[_], M[_], E: Semigroup]
(implicit P: Parallel[M, F]): Parallel[EitherT[M, E, ?], Nested[F, Validated[E, ?], ?]] =
Expand Down
1 change: 1 addition & 0 deletions js/src/test/scala/cats/tests/FutureTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class FutureTests extends CatsSuite {
}
}


implicit val throwableEq: Eq[Throwable] =
Eq.by[Throwable, String](_.toString)

Expand Down
1 change: 1 addition & 0 deletions jvm/src/test/scala/cats/tests/FutureSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class FutureSuite extends CatsSuite {
implicit def cogen[A: Cogen]: Cogen[Future[A]] =
Cogen[Future[A]] { (seed: Seed, t: Future[A]) => Cogen[A].perturb(seed, Await.result(t, timeout)) }


implicit val throwableEq: Eq[Throwable] =
Eq.by[Throwable, String](_.toString)

Expand Down
17 changes: 17 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/Arbitrary.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cats
package laws
package discipline

import cats.data.NonEmptyList.ZipNonEmptyList
import cats.data.NonEmptyVector.ZipNonEmptyVector
import scala.util.{Failure, Success, Try}
import scala.collection.immutable.{SortedMap, SortedSet}
import cats.data._
Expand Down Expand Up @@ -49,13 +51,28 @@ object arbitrary extends ArbitraryInstances0 {
implicit def catsLawsCogenForNonEmptyVector[A](implicit A: Cogen[A]): Cogen[NonEmptyVector[A]] =
Cogen[Vector[A]].contramap(_.toVector)

implicit def catsLawsArbitraryForZipVector[A](implicit A: Arbitrary[A]): Arbitrary[ZipVector[A]] =
Arbitrary(implicitly[Arbitrary[Vector[A]]].arbitrary.map(v => new ZipVector(v)))

implicit def catsLawsArbitraryForZipList[A](implicit A: Arbitrary[A]): Arbitrary[ZipList[A]] =
Arbitrary(implicitly[Arbitrary[List[A]]].arbitrary.map(v => new ZipList(v)))

implicit def catsLawsArbitraryForZipStream[A](implicit A: Arbitrary[A]): Arbitrary[ZipStream[A]] =
Arbitrary(implicitly[Arbitrary[Stream[A]]].arbitrary.map(v => new ZipStream(v)))

implicit def catsLawsArbitraryForZipNonEmptyVector[A](implicit A: Arbitrary[A]): Arbitrary[ZipNonEmptyVector[A]] =
Arbitrary(implicitly[Arbitrary[NonEmptyVector[A]]].arbitrary.map(nev => new ZipNonEmptyVector(nev)))

implicit def catsLawsArbitraryForNonEmptyList[A](implicit A: Arbitrary[A]): Arbitrary[NonEmptyList[A]] =
Arbitrary(implicitly[Arbitrary[List[A]]].arbitrary.flatMap(fa => A.arbitrary.map(a => NonEmptyList(a, fa))))

implicit def catsLawsCogenForNonEmptyList[A](implicit A: Cogen[A]): Cogen[NonEmptyList[A]] =
Cogen[List[A]].contramap(_.toList)


implicit def catsLawsArbitraryForZipNonEmptyList[A](implicit A: Arbitrary[A]): Arbitrary[ZipNonEmptyList[A]] =
Arbitrary(implicitly[Arbitrary[NonEmptyList[A]]].arbitrary.map(nel => new ZipNonEmptyList(nel)))

implicit def catsLawsArbitraryForEitherT[F[_], A, B](implicit F: Arbitrary[F[Either[A, B]]]): Arbitrary[EitherT[F, A, B]] =
Arbitrary(F.arbitrary.map(EitherT(_)))

Expand Down
Loading

0 comments on commit ef64ff8

Please sign in to comment.