Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Parallel type class #1837

Merged
merged 59 commits into from
Oct 26, 2017
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
eaf34e6
Initial version of Parallel
Aug 21, 2017
21942c5
Add Either/Validated Parallel instance
Aug 21, 2017
b37732b
Break up syntax for parallel
Aug 21, 2017
664988a
Add Parallel syntax tests
Aug 21, 2017
e7f6f68
Add Tuple syntax for Parallel
Aug 22, 2017
f37500a
Add ParallelTests
Aug 22, 2017
447d929
Fix Parallel law
Aug 23, 2017
4259554
Add more tests
Aug 23, 2017
6c5efc9
Add Parallel Kleisli instance
Aug 23, 2017
d7ba338
Add instances for OptionT and EitherT to nested
Aug 23, 2017
a7bea82
Add law tests for parallel OptionT and EitherT
Aug 23, 2017
1556909
Make EitherT instance forward to Validated
Aug 23, 2017
b3470db
Add Kleisli lawTest
Aug 23, 2017
b458b3d
Add WriterT instance
Aug 23, 2017
2815a5b
Add more tests
Aug 24, 2017
7844788
Add scaladoc
Aug 24, 2017
378549f
Add ApplicativeError instance for MonadError and Parallel
Aug 26, 2017
1490fe4
Add Test that actually hits the implicated ApplicativeError instance
Aug 26, 2017
2dc71fe
Fix mixup
Aug 28, 2017
00664ae
Move appError instance to Parallel companion object
Aug 29, 2017
f941a0c
Fix apperror test
Aug 29, 2017
1236205
Add sequential roundtrip law
Aug 30, 2017
17c0bc0
Add ZipNEL and ZipNEV and Parallel instances
Sep 2, 2017
1502087
Add law for testing that pure is consistent across Parallel pairs
Sep 3, 2017
f1b1c1a
Add Parallel Serializable tests
Sep 4, 2017
9252e36
Add EitherT Parallel instance that doesn't require a Parallel Instanc…
Sep 5, 2017
e8eb35d
Add ZipVector + Parallel instance
Sep 7, 2017
1a99aab
Add ZipVector test
Sep 10, 2017
ae03b33
Add scaladoc to ApplicativeError and change order of type parameters
Sep 10, 2017
11caba0
Add Parallel#identity function
Sep 10, 2017
1027a41
Add identity instances for Future and Id
Sep 10, 2017
942a152
Simplify parAp2 implementation
Sep 12, 2017
6354e08
Refactor Parallel
Sep 12, 2017
26b7930
Add applicativeError instace method
Sep 12, 2017
9e3891d
Reverse applicativeError and remove redundant .apply
Sep 15, 2017
4dbc995
Simplify further
Sep 15, 2017
61b7cc7
Add FailFastFuture + Parallel instance
Sep 21, 2017
50c5732
Shorten wait times
Sep 22, 2017
c661860
Add ZipStream and OneAnd instance
Sep 22, 2017
ccc5f45
Convert traits to abstract classes
Sep 22, 2017
3a300b4
Merge branch 'master' into add-parallel-class
Sep 22, 2017
ecc8e50
Add consistency test for zip stream
Sep 22, 2017
4882401
Add Applicative test for Applicative[OneAnd]
Sep 22, 2017
50c9619
Add parAp test
Sep 27, 2017
db973c9
Add ZipList and lawtest all Zip* instances
Sep 28, 2017
615a1a5
Add ZipList consistency test
Sep 28, 2017
7395c0a
Add NonEmptyParallel
Sep 28, 2017
a8afdfe
Add test cases for ParNonEmptyTraverse
Sep 29, 2017
7f91072
Update scaladoc
Sep 29, 2017
c5e3423
Remove FailFastFuture and all Zip* instances
Sep 29, 2017
15d9a45
Rename methods in NonEmptyParallel
Sep 30, 2017
ad8a7c4
optimize AppError
Sep 30, 2017
4151e7e
Add parFlatTraverse and sequence
Oct 1, 2017
87f7b87
Merge branch 'master' into add-parallel-class
Oct 14, 2017
4a9f2ad
Add isomorphic functor law
Oct 14, 2017
7a6cd52
Fix law test parameters
Oct 14, 2017
cca85ea
Merge branch 'master' into add-parallel-class
Oct 16, 2017
4bbd46e
Merge branch 'master' into add-parallel-class
Oct 17, 2017
0c87a1b
Merge branch 'master' into add-parallel-class
Oct 19, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 269 additions & 0 deletions core/src/main/scala/cats/Parallel.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
package cats

import cats.arrow.FunctionK

/**
* Some types that form a FlatMap, are also capable of forming an Apply that supports parallel composition.
* The NonEmptyParallel type class allows us to represent this relationship.
*/
trait NonEmptyParallel[M[_], F[_]] extends Serializable {
/**
* The Apply instance for F[_]
*/
def apply: Apply[F]

/**
* The FlatMap instance for M[_]
*/
def flatMap: FlatMap[M]

/**
* Natural Transformation from the parallel Apply F[_] to the sequential FlatMap M[_].
*/
def sequential: F ~> M

/**
* Natural Transformation from the sequential FlatMap M[_] to the parallel Apply F[_].
*/
def parallel: M ~> F

}

/**
* Some types that form a Monad, are also capable of forming an Applicative that supports parallel composition.
* The Parallel type class allows us to represent this relationship.
*/
trait Parallel[M[_], F[_]] extends NonEmptyParallel[M, F] {
/**
* The applicative instance for F[_]
*/
def applicative: Applicative[F]

/**
* The monad instance for M[_]
*/
def monad: Monad[M]

override def apply: Apply[F] = applicative

override def flatMap: FlatMap[M] = monad

/**
* Provides an `ApplicativeError[F, E]` instance for any F, that has a `Parallel[M, F]`
* and a `MonadError[M, E]` instance.
* I.e. if you have a type M[_], that supports parallel composition through type F[_],
* then you can get `ApplicativeError[F, E]` from `MonadError[M, E]`.
*/
def applicativeError[E](implicit E: MonadError[M, E]): ApplicativeError[F, E] = new ApplicativeError[F, E] {

def raiseError[A](e: E): F[A] =
parallel(MonadError[M, E].raiseError(e))

def handleErrorWith[A](fa: F[A])(f: (E) => F[A]): F[A] = {
val ma = MonadError[M, E].handleErrorWith(sequential(fa))(f andThen sequential.apply)
parallel(ma)
}

def pure[A](x: A): F[A] = applicative.pure(x)

def ap[A, B](ff: F[(A) => B])(fa: F[A]): F[B] = applicative.ap(ff)(fa)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should override basically everything here or we could de-optimize an existing applicative.


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

override def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] = applicative.product(fa, fb)

override def map2[A, B, Z](fa: F[A], fb: F[B])(f: (A, B) => Z): F[Z] = applicative.map2(fa, fb)(f)

override def map2Eval[A, B, Z](fa: F[A], fb: Eval[F[B]])(f: (A, B) => Z): Eval[F[Z]] =
applicative.map2Eval(fa, fb)(f)

override def unlessA[A](cond: Boolean)(f: => F[A]): F[Unit] = applicative.unlessA(cond)(f)

override def whenA[A](cond: Boolean)(f: => F[A]): F[Unit] = applicative.whenA(cond)(f)
}
}

object NonEmptyParallel {
def apply[M[_], F[_]](implicit P: NonEmptyParallel[M, F]): NonEmptyParallel[M, F] = P
}

object Parallel extends ParallelArityFunctions {

def apply[M[_], F[_]](implicit P: Parallel[M, F]): Parallel[M, F] = P

/**
* Like `Traverse[A].sequence`, but uses the applicative instance
* corresponding to the Parallel instance instead.
*/
def parSequence[T[_]: Traverse, M[_], F[_], A]
(tma: T[M[A]])(implicit P: Parallel[M, F]): M[T[A]] = {
val fta: F[T[A]] = Traverse[T].traverse(tma)(P.parallel.apply)(P.applicative)
P.sequential(fta)
}

/**
* Like `Traverse[A].traverse`, but uses the applicative instance
* corresponding to the Parallel instance instead.
*/
def parTraverse[T[_]: Traverse, M[_], F[_], A, B]
(ta: T[A])(f: A => M[B])(implicit P: Parallel[M, F]): M[T[B]] = {
val gtb: F[T[B]] = Traverse[T].traverse(ta)(f andThen P.parallel.apply)(P.applicative)
P.sequential(gtb)
}

/**
* Like `Traverse[A].flatTraverse`, but uses the applicative instance
* corresponding to the Parallel instance instead.
*/
def parFlatTraverse[T[_]: Traverse: FlatMap, M[_], F[_], A, B]
(ta: T[A])(f: A => M[T[B]])(implicit P: Parallel[M, F]): M[T[B]] = {
val gtb: F[T[B]] = Traverse[T].flatTraverse(ta)(f andThen P.parallel.apply)(P.applicative, FlatMap[T])
P.sequential(gtb)
}

/**
* Like `Traverse[A].flatSequence`, but uses the applicative instance
* corresponding to the Parallel instance instead.
*/
def parFlatSequence[T[_]: Traverse: FlatMap, M[_], F[_], A]
(tma: T[M[T[A]]])(implicit P: Parallel[M, F]): M[T[A]] = {
val fta: F[T[A]] = Traverse[T].flatTraverse(tma)(P.parallel.apply)(P.applicative, FlatMap[T])
P.sequential(fta)
}

/**
* Like `Foldable[A].sequence_`, but uses the applicative instance
* corresponding to the Parallel instance instead.
*/
def parSequence_[T[_]: Foldable, M[_], F[_], A]
(tma: T[M[A]])(implicit P: Parallel[M, F]): M[Unit] = {
val fu: F[Unit] = Foldable[T].traverse_(tma)(P.parallel.apply)(P.applicative)
P.sequential(fu)
}

/**
* Like `Foldable[A].traverse_`, but uses the applicative instance
* corresponding to the Parallel instance instead.
*/
def parTraverse_[T[_]: Foldable, M[_], F[_], A, B]
(ta: T[A])(f: A => M[B])(implicit P: Parallel[M, F]): M[Unit] = {
val gtb: F[Unit] = Foldable[T].traverse_(ta)(f andThen P.parallel.apply)(P.applicative)
P.sequential(gtb)
}

/**
* Like `NonEmptyTraverse[A].nonEmptySequence`, but uses the apply instance
* corresponding to the Parallel instance instead.
*/
def parNonEmptySequence[T[_]: NonEmptyTraverse, M[_], F[_], A]
(tma: T[M[A]])(implicit P: NonEmptyParallel[M, F]): M[T[A]] = {
val fta: F[T[A]] = NonEmptyTraverse[T].nonEmptyTraverse(tma)(P.parallel.apply)(P.apply)
P.sequential(fta)
}

/**
* Like `NonEmptyTraverse[A].nonEmptyTraverse`, but uses the apply instance
* corresponding to the Parallel instance instead.
*/
def parNonEmptyTraverse[T[_]: NonEmptyTraverse, M[_], F[_], A, B]
(ta: T[A])(f: A => M[B])(implicit P: NonEmptyParallel[M, F]): M[T[B]] = {
val gtb: F[T[B]] = NonEmptyTraverse[T].nonEmptyTraverse(ta)(f andThen P.parallel.apply)(P.apply)
P.sequential(gtb)
}


/**
* Like `NonEmptyTraverse[A].nonEmptyFlatTraverse`, but uses the apply instance
* corresponding to the Parallel instance instead.
*/
def parNonEmptyFlatTraverse[T[_]: NonEmptyTraverse: FlatMap, M[_], F[_], A, B]
(ta: T[A])(f: A => M[T[B]])(implicit P: NonEmptyParallel[M, F]): M[T[B]] = {
val gtb: F[T[B]] = NonEmptyTraverse[T].nonEmptyFlatTraverse(ta)(f andThen P.parallel.apply)(P.apply, FlatMap[T])
P.sequential(gtb)
}


/**
* Like `NonEmptyTraverse[A].nonEmptyFlatSequence`, but uses the apply instance
* corresponding to the Parallel instance instead.
*/
def parNonEmptyFlatSequence[T[_]: NonEmptyTraverse: FlatMap, M[_], F[_], A]
(tma: T[M[T[A]]])(implicit P: NonEmptyParallel[M, F]): M[T[A]] = {
val fta: F[T[A]] = NonEmptyTraverse[T].nonEmptyFlatTraverse(tma)(P.parallel.apply)(P.apply, FlatMap[T])
P.sequential(fta)
}

/**
* Like `Reducible[A].nonEmptySequence_`, but uses the apply instance
* corresponding to the Parallel instance instead.
*/
def parNonEmptySequence_[T[_]: Reducible, M[_], F[_], A]
(tma: T[M[A]])(implicit P: NonEmptyParallel[M, F]): M[Unit] = {
val fu: F[Unit] = Reducible[T].nonEmptyTraverse_(tma)(P.parallel.apply)(P.apply)
P.sequential(fu)
}

/**
* Like `Reducible[A].nonEmptyTraverse_`, but uses the apply instance
* corresponding to the Parallel instance instead.
*/
def parNonEmptyTraverse_[T[_]: Reducible, M[_], F[_], A, B]
(ta: T[A])(f: A => M[B])(implicit P: NonEmptyParallel[M, F]): M[Unit] = {
val gtb: F[Unit] = Reducible[T].nonEmptyTraverse_(ta)(f andThen P.parallel.apply)(P.apply)
P.sequential(gtb)
}

/**
* Like `Applicative[F].ap`, but uses the applicative instance
* corresponding to the Parallel instance instead.
*/
def parAp[M[_], F[_], A, B](mf: M[A => B])
(ma: M[A])
(implicit P: NonEmptyParallel[M, F]): M[B] =
P.sequential(P.apply.ap(P.parallel(mf))(P.parallel(ma)))

/**
* Like `Applicative[F].product`, but uses the applicative instance
* corresponding to the Parallel instance instead.
*/
def parProduct[M[_], F[_], A, B](ma: M[A], mb: M[B])
(implicit P: NonEmptyParallel[M, F]): M[(A, B)] =
P.sequential(P.apply.product(P.parallel(ma), P.parallel(mb)))

/**
* Like `Applicative[F].ap2`, but uses the applicative instance
* corresponding to the Parallel instance instead.
*/
def parAp2[M[_], F[_], A, B, Z](ff: M[(A, B) => Z])
(ma: M[A], mb: M[B])
(implicit P: NonEmptyParallel[M, F]): M[Z] =
P.sequential(
P.apply.ap2(P.parallel(ff))(P.parallel(ma), P.parallel(mb))
)

/**
* Provides an `ApplicativeError[F, E]` instance for any F, that has a `Parallel[M, F]`
* and a `MonadError[M, E]` instance.
* I.e. if you have a type M[_], that supports parallel composition through type F[_],
* then you can get `ApplicativeError[F, E]` from `MonadError[M, E]`.
*/
def applicativeError[M[_], F[_], E]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I made the suggestion, I was thinking having this method on Parallel instance. I.e.

trait Parallel[M[_], F[_]] extends Serializable {
    ....
   def parallelError[E](implicit ME: MonadError[M, E]):  ApplicativeError[F, E] = ...
}

which feels a natural addition to the existing API. Your API has benefits too. What do you think? Maybe we have both API?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think yours is definitely more useful, but I'd be okay with having both :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

(implicit P: Parallel[M, F], E: MonadError[M, E]): ApplicativeError[F, E] = P.applicativeError

/**
* A Parallel instance for any type `M[_]` that supports parallel composition through itself.
* Can also be used for giving `Parallel` instances to types that do not support parallel composition,
* but are required to have an instance of `Parallel` defined,
* in which case parallel composition will actually be sequential.
*/
def identity[M[_]: Monad]: Parallel[M, M] = new Parallel[M, M] {

val monad: Monad[M] = implicitly[Monad[M]]

val applicative: Applicative[M] = implicitly[Monad[M]]

val sequential: M ~> M = FunctionK.id

val parallel: M ~> M = FunctionK.id
}
}
14 changes: 14 additions & 0 deletions core/src/main/scala/cats/data/Kleisli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ private[data] sealed abstract class KleisliInstances0 extends KleisliInstances1
private[data] sealed abstract class KleisliInstances1 extends KleisliInstances2 {
implicit def catsDataMonadForKleisli[F[_], A](implicit M: Monad[F]): Monad[Kleisli[F, A, ?]] =
new KleisliMonad[F, A] { def F: Monad[F] = M }

implicit def catsDataParallelForKleisli[F[_], M[_], A]
(implicit P: Parallel[M, F]): Parallel[Kleisli[M, A, ?], Kleisli[F, A, ?]] = new Parallel[Kleisli[M, A, ?], Kleisli[F, A, ?]]{
implicit val appF = P.applicative
implicit val monadM = P.monad
def applicative: Applicative[Kleisli[F, A, ?]] = catsDataApplicativeForKleisli
def monad: Monad[Kleisli[M, A, ?]] = catsDataMonadForKleisli

def sequential: Kleisli[F, A, ?] ~> Kleisli[M, A, ?] =
λ[Kleisli[F, A, ?] ~> Kleisli[M, A, ?]](_.transform(P.sequential))

def parallel: Kleisli[M, A, ?] ~> Kleisli[F, A, ?] =
λ[Kleisli[M, A, ?] ~> Kleisli[F, A, ?]](_.transform(P.parallel))
}
}

private[data] sealed abstract class KleisliInstances2 extends KleisliInstances3 {
Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/cats/data/NonEmptyList.scala
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ object NonEmptyList extends NonEmptyListInstances {

def fromReducible[F[_], A](fa: F[A])(implicit F: Reducible[F]): NonEmptyList[A] =
F.toNonEmptyList(fa)

}

private[data] sealed abstract class NonEmptyListInstances extends NonEmptyListInstances0 {
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyVector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ private[data] sealed abstract class NonEmptyVectorInstances {
implicit def catsDataSemigroupForNonEmptyVector[A]: Semigroup[NonEmptyVector[A]] =
catsDataInstancesForNonEmptyVector.algebra


}

object NonEmptyVector extends NonEmptyVectorInstances with Serializable {
Expand All @@ -341,4 +342,6 @@ object NonEmptyVector extends NonEmptyVectorInstances with Serializable {
def fromVectorUnsafe[A](vector: Vector[A]): NonEmptyVector[A] =
if (vector.nonEmpty) new NonEmptyVector(vector)
else throw new IllegalArgumentException("Cannot create NonEmptyVector from empty vector")


}
7 changes: 7 additions & 0 deletions core/src/main/scala/cats/data/OneAnd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,10 @@ final case class OneAnd[F[_], A](head: A, tail: F[A]) {
s"OneAnd(${A.show(head)}, ${FA.show(tail)})"
}


private[data] sealed abstract class OneAndInstances extends OneAndLowPriority3 {


implicit def catsDataEqForOneAnd[A, F[_]](implicit A: Eq[A], FA: Eq[F[A]]): Eq[OneAnd[F, A]] =
new Eq[OneAnd[F, A]]{
def eqv(x: OneAnd[F, A], y: OneAnd[F, A]): Boolean = x === y
Expand Down Expand Up @@ -199,7 +201,9 @@ private[data] sealed abstract class OneAndLowPriority0 {
}
}


private[data] sealed abstract class OneAndLowPriority1 extends OneAndLowPriority0 {

implicit def catsDataFunctorForOneAnd[F[_]](implicit F: Functor[F]): Functor[OneAnd[F, ?]] =
new Functor[OneAnd[F, ?]] {
def map[A, B](fa: OneAnd[F, A])(f: A => B): OneAnd[F, B] =
Expand All @@ -209,6 +213,7 @@ private[data] sealed abstract class OneAndLowPriority1 extends OneAndLowPriority
}

private[data] sealed abstract class OneAndLowPriority2 extends OneAndLowPriority1 {

implicit def catsDataTraverseForOneAnd[F[_]](implicit F: Traverse[F]): Traverse[OneAnd[F, ?]] =
new Traverse[OneAnd[F, ?]] {
def traverse[G[_], A, B](fa: OneAnd[F, A])(f: (A) => G[B])(implicit G: Applicative[G]): G[OneAnd[F, B]] = {
Expand All @@ -225,7 +230,9 @@ private[data] sealed abstract class OneAndLowPriority2 extends OneAndLowPriority
}
}


private[data] sealed abstract class OneAndLowPriority3 extends OneAndLowPriority2 {

implicit def catsDataNonEmptyTraverseForOneAnd[F[_]](implicit F: Traverse[F], F2: Alternative[F]): NonEmptyTraverse[OneAnd[F, ?]] =
new NonEmptyReducible[OneAnd[F, ?], F] with NonEmptyTraverse[OneAnd[F, ?]] {
def nonEmptyTraverse[G[_], A, B](fa: OneAnd[F, A])(f: (A) => G[B])(implicit G: Apply[G]): G[OneAnd[F, B]] = {
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/scala/cats/data/WriterT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@ private[data] sealed abstract class WriterTInstances0 extends WriterTInstances1
implicit val L0: Monoid[L] = L
}

implicit def catsDataParallelForWriterT[F[_], M[_], L: Monoid]
(implicit P: Parallel[M, F]): Parallel[WriterT[M, L, ?], WriterT[F, L, ?]] = new Parallel[WriterT[M, L, ?], WriterT[F, L, ?]]{
implicit val appF = P.applicative
implicit val monadM = P.monad

def applicative: Applicative[WriterT[F, L, ?]] = catsDataApplicativeForWriterT
def monad: Monad[WriterT[M, L, ?]] = catsDataMonadForWriterT

def sequential: WriterT[F, L, ?] ~> WriterT[M, L, ?] =
λ[WriterT[F, L, ?] ~> WriterT[M, L, ?]](wfl => WriterT(P.sequential(wfl.run)))

def parallel: WriterT[M, L, ?] ~> WriterT[F, L, ?] =
λ[WriterT[M, L, ?] ~> WriterT[F, L, ?]](wml => WriterT(P.parallel(wml.run)))
}

implicit def catsDataEqForWriterTId[L: Eq, V: Eq]: Eq[WriterT[Id, L, V]] =
catsDataEqForWriterT[Id, L, V]

Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/cats/instances/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ trait AllInstances
with OptionInstances
with OrderInstances
with OrderingInstances
with ParallelInstances
with PartialOrderInstances
with PartialOrderingInstances
with QueueInstances
Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/cats/instances/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ package object instances {
object option extends OptionInstances
object order extends OrderInstances
object ordering extends OrderingInstances
object parallel extends ParallelInstances
object partialOrder extends PartialOrderInstances
object partialOrdering extends PartialOrderingInstances
object queue extends QueueInstances
Expand Down
Loading