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 distributive typeclass and some instances #2046

Merged
merged 14 commits into from
Dec 12, 2017
13 changes: 12 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,18 @@ def mimaSettings(moduleName: String) = Seq(
exclude[ReversedMissingMethodProblem]("cats.MonadError.rethrow"),
exclude[ReversedMissingMethodProblem]("cats.syntax.MonadErrorSyntax.catsSyntaxMonadErrorRethrow"),
exclude[DirectMissingMethodProblem]("cats.data.CokleisliArrow.id"),
exclude[IncompatibleResultTypeProblem]("cats.data.CokleisliArrow.id")
exclude[IncompatibleResultTypeProblem]("cats.data.CokleisliArrow.id"),
exclude[InheritedNewAbstractMethodProblem]("cats.Distributive#ToDistributiveOps.toDistributiveOps"),
exclude[InheritedNewAbstractMethodProblem]("cats.syntax.DistributiveSyntax.catsSyntaxDistributiveOps"),
exclude[InheritedNewAbstractMethodProblem]("cats.instances.Function0Instances0.function0Distributive"),
exclude[InheritedNewAbstractMethodProblem]("cats.instances.Function1Instances0.functior1Distributive"),
exclude[InheritedNewAbstractMethodProblem]("cats.instances.Function0Instances0.function0Distributive"),
exclude[InheritedNewAbstractMethodProblem]("cats.instances.Function1Instances0.functior1Distributive"),
exclude[InheritedNewAbstractMethodProblem]("cats.instances.Function1Instances0.functior1Distributive"),
exclude[InheritedNewAbstractMethodProblem]("cats.instances.Function0Instances0.function0Distributive"),
exclude[InheritedNewAbstractMethodProblem]("cats.instances.Function1Instances0.catsStdDistributiveForFunction1"),
exclude[InheritedNewAbstractMethodProblem]("cats.instances.Function1Instances0.catsStdDistributiveForFunction1"),
exclude[InheritedNewAbstractMethodProblem]("cats.instances.Function1Instances0.catsStdDistributiveForFunction1")
)
}
)
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/scala/cats/Composed.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package cats


private[cats] trait ComposedDistributive[F[_], G[_]] extends Distributive[λ[α => F[G[α]]]] with ComposedFunctor[F, G] { outer =>
def F: Distributive[F]
def G: Distributive[G]

override def distribute[H[_]: Functor, A, B](ha: H[A])(f: A => F[G[B]]): F[G[H[B]]] =
F.map(F.distribute(ha)(f))(G.cosequence(_))
}

private[cats] trait ComposedInvariant[F[_], G[_]] extends Invariant[λ[α => F[G[α]]]] { outer =>
def F: Invariant[F]
def G: Invariant[G]
Expand Down
22 changes: 22 additions & 0 deletions core/src/main/scala/cats/Distributive.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package cats
import simulacrum.typeclass

@typeclass trait Distributive[F[_]] extends Functor[F] { self =>

/**
* Given a function which returns a distributive `F`, apply that value across the structure G.
*/
def distribute[G[_]: Functor, A, B](ga: G[A])(f: A => F[B]): F[G[B]]

/**
* Given a Functor G which wraps some distributive F, distribute F across the G.
*/
def cosequence[G[_]: Functor, A](ga: G[F[A]]): F[G[A]] = distribute(ga)(identity)

// Distributive composes
def compose[G[_]](implicit G0: Distributive[G]): Distributive[λ[α => F[G[α]]]] =
new ComposedDistributive[F, G] {
implicit def F = self
implicit def G = G0
}
}
17 changes: 16 additions & 1 deletion core/src/main/scala/cats/data/Kleisli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,12 @@ private[data] sealed abstract class KleisliInstances6 extends KleisliInstances7
new KleisliApply[F, A] { def F: Apply[F] = A }
}

private[data] sealed abstract class KleisliInstances7 {
private[data] sealed abstract class KleisliInstances7 extends KleisliInstances8 {
implicit def catsDataDistributiveForKleisli[F[_], R](implicit F0: Distributive[F]): Distributive[Kleisli[F, R, ?]] =
new KleisliDistributive[F, R] { implicit def F: Distributive[F] = F0 }
}

private[data] sealed abstract class KleisliInstances8 {
implicit def catsDataFunctorForKleisli[F[_], A](implicit F0: Functor[F]): Functor[Kleisli[F, A, ?]] =
new KleisliFunctor[F, A] { def F: Functor[F] = F0 }
}
Expand Down Expand Up @@ -379,3 +384,13 @@ private[data] trait KleisliFunctor[F[_], A] extends Functor[Kleisli[F, A, ?]] {
override def map[B, C](fa: Kleisli[F, A, B])(f: B => C): Kleisli[F, A, C] =
fa.map(f)
}

private trait KleisliDistributive[F[_], R] extends Distributive[Kleisli[F, R, ?]] {
implicit def F: Distributive[F]

override def distribute[G[_]: Functor, A, B](a: G[A])(f: A => Kleisli[F, R, B]): Kleisli[F, R, G[B]] =
Kleisli(r => F.distribute(a)(f(_) run r))


def map[A, B](fa: Kleisli[F, R, A])(f: A => B): Kleisli[F, R, B] = fa.map(f)
}
12 changes: 12 additions & 0 deletions core/src/main/scala/cats/data/Nested.scala
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ private[data] sealed abstract class NestedInstances8 extends NestedInstances9 {
new NestedApply[F, G] {
val FG: Apply[λ[α => F[G[α]]]] = Apply[F].compose[G]
}

implicit def catsDataDistributiveForNested[F[_]: Distributive, G[_]: Distributive]: Distributive[Nested[F, G, ?]] =
new NestedDistributive[F, G] {
val FG: Distributive[λ[α => F[G[α]]]] = Distributive[F].compose[G]
}
}

private[data] sealed abstract class NestedInstances9 extends NestedInstances10 {
Expand Down Expand Up @@ -245,6 +250,13 @@ private[data] trait NestedTraverse[F[_], G[_]] extends Traverse[Nested[F, G, ?]]
Applicative[H].map(FG.traverse(fga.value)(f))(Nested(_))
}

private[data] trait NestedDistributive[F[_], G[_]] extends Distributive[Nested[F, G, ?]] with NestedFunctor[F, G] {
def FG: Distributive[λ[α => F[G[α]]]]

def distribute[H[_]: Functor, A, B](ha: H[A])(f: A => Nested[F, G, B]): Nested[F, G, H[B]] =
Nested(FG.distribute(ha) { a => f(a).value })
}

private[data] trait NestedReducible[F[_], G[_]] extends Reducible[Nested[F, G, ?]] with NestedFoldable[F, G] {
def FG: Reducible[λ[α => F[G[α]]]]

Expand Down
16 changes: 15 additions & 1 deletion core/src/main/scala/cats/data/Tuple2K.scala
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,14 @@ private[data] sealed abstract class Tuple2KInstances6 extends Tuple2KInstances7
}
}

private[data] sealed abstract class Tuple2KInstances7 {
private[data] sealed abstract class Tuple2KInstances7 extends Tuple2KInstances8 {
implicit def catsDataDistributiveForTuple2K[F[_], G[_]](implicit FF: Distributive[F], GG: Distributive[G]): Distributive[λ[α => Tuple2K[F, G, α]]] = new Tuple2KDistributive[F, G] {
def F: Distributive[F] = FF
def G: Distributive[G] = GG
}
}

private[data] sealed abstract class Tuple2KInstances8 {
implicit def catsDataFunctorForTuple2K[F[_], G[_]](implicit FF: Functor[F], GG: Functor[G]): Functor[λ[α => Tuple2K[F, G, α]]] = new Tuple2KFunctor[F, G] {
def F: Functor[F] = FF
def G: Functor[G] = GG
Expand All @@ -122,6 +128,14 @@ private[data] sealed trait Tuple2KFunctor[F[_], G[_]] extends Functor[λ[α => T
override def map[A, B](fa: Tuple2K[F, G, A])(f: A => B): Tuple2K[F, G, B] = Tuple2K(F.map(fa.first)(f), G.map(fa.second)(f))
}


private[data] sealed trait Tuple2KDistributive[F[_], G[_]] extends Distributive[λ[α => Tuple2K[F, G, α]]] {
def F: Distributive[F]
def G: Distributive[G]
override def distribute[H[_]: Functor, A, B](ha: H[A])(f: A => Tuple2K[F, G, B]): Tuple2K[F, G, H[B]] = Tuple2K(F.distribute(ha){a => f(a).first}, G.distribute(ha){a => f(a).second})
override def map[A, B](fa: Tuple2K[F, G, A])(f: A => B): Tuple2K[F, G, B] = Tuple2K(F.map(fa.first)(f), G.map(fa.second)(f))
}

private[data] sealed trait Tuple2KContravariant[F[_], G[_]] extends Contravariant[λ[α => Tuple2K[F, G, α]]] {
def F: Contravariant[F]
def G: Contravariant[G]
Expand Down
18 changes: 16 additions & 2 deletions core/src/main/scala/cats/instances/function.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import annotation.tailrec
trait FunctionInstances extends cats.kernel.instances.FunctionInstances
with Function0Instances with Function1Instances

private[instances] sealed trait Function0Instances {
private[instances] sealed trait Function0Instances extends Function0Instances0 {
implicit val catsStdBimonadForFunction0: Bimonad[Function0] =
new Bimonad[Function0] {
def extract[A](x: () => A): A = x()
Expand All @@ -35,6 +35,14 @@ private[instances] sealed trait Function0Instances {
}
}

private[instances] sealed trait Function0Instances0 {
implicit def function0Distributive: Distributive[Function0] = new Distributive[Function0] {
def distribute[F[_]: Functor, A, B](fa: F[A])(f: A => Function0[B]): Function0[F[B]] = {() => Functor[F].map(fa)(a => f(a)()) }

def map[A, B](fa: Function0[A])(f: A => B): Function0[B] = () => f(fa())
}
}

private[instances] sealed trait Function1Instances extends Function1Instances0 {
implicit def catsStdContravariantMonoidalForFunction1[R: Monoid]: ContravariantMonoidal[? => R] =
new ContravariantMonoidal[? => R] {
Expand Down Expand Up @@ -68,6 +76,7 @@ private[instances] sealed trait Function1Instances extends Function1Instances0 {
}
}


implicit val catsStdInstancesForFunction1: Choice[Function1] with CommutativeArrow[Function1] =
new Choice[Function1] with CommutativeArrow[Function1] {
def choice[A, B, C](f: A => C, g: B => C): Either[A, B] => C = {
Expand All @@ -92,11 +101,16 @@ private[instances] sealed trait Function1Instances extends Function1Instances0 {
Category[Function1].algebraK
}


private[instances] sealed trait Function1Instances0 {
implicit def catsStdContravariantForFunction1[R]: Contravariant[? => R] =
new Contravariant[? => R] {
def contramap[T1, T0](fa: T1 => R)(f: T0 => T1): T0 => R =
fa.compose(f)
}

implicit def catsStdDistributiveForFunction1[T1]: Distributive[T1 => ?] = new Distributive[T1 => ?] {
def distribute[F[_]: Functor, A, B](fa: F[A])(f: A => (T1 => B)): T1 => F[B] = {t1 => Functor[F].map(fa)(a => f(a)(t1)) }

def map[A, B](fa: T1 => A)(f: A => B): T1 => B = {t1 => f(fa(t1))}
}
}
5 changes: 3 additions & 2 deletions core/src/main/scala/cats/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ package object cats {
*/
type Id[A] = A
type Endo[A] = A => A
implicit val catsInstancesForId: Bimonad[Id] with CommutativeMonad[Id] with Comonad[Id] with NonEmptyTraverse[Id] =
new Bimonad[Id] with CommutativeMonad[Id] with Comonad[Id] with NonEmptyTraverse[Id] {
implicit val catsInstancesForId: Bimonad[Id] with CommutativeMonad[Id] with Comonad[Id] with NonEmptyTraverse[Id] with Distributive[Id] =
new Bimonad[Id] with CommutativeMonad[Id] with Comonad[Id] with NonEmptyTraverse[Id] with Distributive[Id] {
def pure[A](a: A): A = a
def extract[A](a: A): A = a
def flatMap[A, B](a: A)(f: A => B): B = f(a)
Expand All @@ -43,6 +43,7 @@ package object cats {
case Left(a1) => tailRecM(a1)(f)
case Right(b) => b
}
override def distribute[F[_], A, B](fa: F[A])(f: A => B)(implicit F: Functor[F]): Id[F[B]] = F.map(fa)(f)
override def map[A, B](fa: A)(f: A => B): B = f(fa)
override def ap[A, B](ff: A => B)(fa: A): B = ff(fa)
override def flatten[A](ffa: A): A = ffa
Expand Down
14 changes: 14 additions & 0 deletions core/src/main/scala/cats/syntax/DistributiveSyntax.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package cats
package syntax

import cats.evidence.===

trait DistributiveSyntax extends Distributive.ToDistributiveOps {
implicit final def catsSyntaxDistributiveOps[F[_]: Functor, A](fa: F[A]): DistributiveOps[F, A] = new DistributiveOps[F, A](fa)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should add another class for cosequence, we wouldn't need cats.evidence then:

final class CosequenceOps[F[_]: Functor, G[_]: Distributive, A](val fga: F[G[A]]) extends AnyVal {
  def cosequence: G[F[A]] = G.cosequence(fga)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmm, not sure I see a reason why that's better? The evidence constraint is on just the one method and it saves the extra class for syntax. Is there a reason it should be preferred?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe you're right, disregard me! :D

}

// Add syntax to functor as part of importing distributive syntax.
final class DistributiveOps[F[_], A](val fa: F[A]) extends AnyVal {
def distribute[G[_], B](f: A => G[B])(implicit G: Distributive[G], F: Functor[F]): G[F[B]] = G.distribute(fa)(f)
def cosequence[G[_], B](implicit G: Distributive[G], F: Functor[F], ev: A === G[B]): G[F[B]] = G.cosequence(ev.substitute(fa))
}
1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ trait AllSyntax
with ComonadSyntax
with ComposeSyntax
with ContravariantSyntax
with DistributiveSyntax
with ContravariantMonoidalSyntax
with ContravariantSemigroupalSyntax
with EitherKSyntax
Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package object syntax {
@deprecated("use cats.syntax.semigroupal instead", "1.0.0-RC1")
object cartesian extends SemigroupalSyntax
object coflatMap extends CoflatMapSyntax
object distributive extends DistributiveSyntax
object eitherK extends EitherKSyntax
object comonad extends ComonadSyntax
object compose extends ComposeSyntax
Expand Down
37 changes: 37 additions & 0 deletions laws/src/main/scala/cats/laws/DistributiveLaws.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cats
package laws

import cats.Id
import cats.data.Nested
import cats.syntax.distributive._

trait DistributiveLaws[F[_]] extends FunctorLaws[F] {
implicit override def F: Distributive[F]

def cosequenceIdentity[A](fa: F[A]): IsEq[F[A]] = {
F.cosequence[Id, A](fa) <-> fa
}

def cosequenceTwiceIsId[A, M[_]](fma: F[M[A]])(implicit M: Distributive[M]): IsEq[F[M[A]]] = {
val result = F.cosequence(M.cosequence(fma))
fma <-> result
}

def composition[A, B, C, M[_], N[_]](
ma: M[A],
f: A => F[B],
g: B => N[C]
)(implicit
N: Distributive[N],
M: Functor[M]
): IsEq[Nested[F, N, M[C]]] = {
val rhs = ma.distribute[Nested[F, N, ?], C](a => Nested(F.map(f(a))(g)))
val lhs = Nested(F.map(ma.distribute(f))(fb => fb.distribute(g)))
lhs <-> rhs
}
}

object DistributiveLaws {
def apply[F[_]](implicit ev: Distributive[F]): DistributiveLaws[F] =
new DistributiveLaws[F] { def F: Distributive[F] = ev }
}
43 changes: 43 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/DistributiveTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cats
package laws
package discipline

import org.scalacheck.{Arbitrary, Cogen, Prop}
import Prop._

trait DistributiveTests[F[_]] extends FunctorTests[F] {
def laws: DistributiveLaws[F]

def distributive[A: Arbitrary, B: Arbitrary, C: Arbitrary, X[_]: Functor, Y[_]: Distributive](implicit
ArbFA: Arbitrary[F[A]],
ArbFB: Arbitrary[F[B]],
ArbXA: Arbitrary[X[A]],
ArbYC: Arbitrary[Y[C]],
ArbFYA: Arbitrary[F[Y[A]]],
CogenA: Cogen[A],
CogenB: Cogen[B],
CogenC: Cogen[C],
EqFA: Eq[F[A]],
EqFC: Eq[F[C]],
EqA: Eq[A],
EqFYXC: Eq[F[Y[X[C]]]],
EqFYA: Eq[F[Y[A]]],
EqYFB: Eq[Y[F[B]]]
): RuleSet = {
new RuleSet {
def name: String = "distributive"
def bases: Seq[(String, RuleSet)] = Nil
def parents: Seq[RuleSet] = Seq(functor[A, B, C])
def props: Seq[(String, Prop)] = Seq(
"distributive identity" -> forAll(laws.cosequenceIdentity[A] _),
"distributive composition" -> forAll(laws.composition[A, B, C, X, Y] _),
"distributive double cosequence identity" -> forAll(laws.cosequenceTwiceIsId[A, Y] _)
)
}
}
}

object DistributiveTests {
def apply[F[_]: Distributive]: DistributiveTests[F] =
new DistributiveTests[F] { def laws: DistributiveLaws[F] = DistributiveLaws[F] }
}
7 changes: 7 additions & 0 deletions tests/src/test/scala/cats/tests/FunctionSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class FunctionSuite extends CatsSuite {
checkAll("Function1[Int, Int]", MonoidKTests[λ[α => α => α]].monoidK[Int])
checkAll("MonoidK[λ[α => α => α]", SerializableTests.serializable(catsStdMonoidKForFunction1))

checkAll("Function1[Int, Int]", DistributiveTests[Int => ?].distributive[Int, Int, Int, Id, Function1[Int, ?]])
checkAll("Distributive[Int => ?]", SerializableTests.serializable(Distributive[Int => ?]))



// law checks for the various Function0-related instances
checkAll("Function0[Eqed]", EqTests[Function0[Eqed]].eqv)
Expand All @@ -68,6 +72,8 @@ class FunctionSuite extends CatsSuite {
checkAll("Function0[CMono]", CommutativeMonoidTests[Function0[CMono]].commutativeMonoid)
checkAll("Function0[Grp]", GroupTests[Function0[Grp]].group)
checkAll("Function0[CGrp]", CommutativeGroupTests[Function0[CGrp]].commutativeGroup)
checkAll("Function0[Distributive]", DistributiveTests[Function0].distributive[Int, Int, Int, Id, Function0])


test("Function0[Hsh]") {
forAll { (x: Function0[Hsh], y: Function0[Hsh]) =>
Expand All @@ -93,6 +99,7 @@ class FunctionSuite extends CatsSuite {
checkAll("CommutativeMonoid[() => CMono]", SerializableTests.serializable(CommutativeMonoid[() => CMono]))
checkAll("Group[() => Grp]", SerializableTests.serializable(Group[() => Grp]))
checkAll("CommutativeGroup[() => CGrp]", SerializableTests.serializable(CommutativeGroup[() => CGrp]))
checkAll("Function0", SerializableTests.serializable(Distributive[Function0]))

// law checks for the various Function1-related instances
checkAll("Function1[String, Semi]", SemigroupTests[Function1[String, Semi]].semigroup)
Expand Down
7 changes: 7 additions & 0 deletions tests/src/test/scala/cats/tests/KleisliSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ class KleisliSuite extends CatsSuite {
checkAll("Functor[Kleisli[Option, Int, ?]]", SerializableTests.serializable(Functor[Kleisli[Option, Int, ?]]))
}

{
checkAll("Kleisli[Function0, Int, ?]", DistributiveTests[Kleisli[Function0, Int, ?]].distributive[Int, Int, Int, Option, Id])
checkAll("Distributive[Kleisli[Function0, Int, ?]]", SerializableTests.serializable(Distributive[Kleisli[Function0, Int, ?]]))
}

{
implicit val catsDataMonoidForKleisli = Kleisli.catsDataMonoidForKleisli[Option, Int, String]
checkAll("Kleisli[Option, Int, String]", MonoidTests[Kleisli[Option, Int, String]].monoid)
Expand Down Expand Up @@ -296,5 +301,7 @@ class KleisliSuite extends CatsSuite {
ApplicativeError[Kleisli[cats.data.Validated[Unit, ?], Int, ?], Unit]
ApplicativeError[Kleisli[Option, Int, ?], Unit]
MonadError[Kleisli[Option, Int, ?], Unit]

Distributive[Kleisli[Function0, Int, ?]]
}
}
7 changes: 7 additions & 0 deletions tests/src/test/scala/cats/tests/NestedSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,11 @@ class NestedSuite extends CatsSuite {
checkAll("Nested[ListWrapper, Option, ?]", MonoidKTests[Nested[ListWrapper, Option, ?]].monoidK[Int])
checkAll("MonoidK[Nested[ListWrapper, Option, ?]]", SerializableTests.serializable(MonoidK[Nested[ListWrapper, Option, ?]]))
}

{
import cats.laws.discipline.eq._
//Distributive composition
checkAll("Nested[Function1[Int, ?], Function0, ?]", DistributiveTests[Nested[Function1[Int, ?], Function0, ?]].distributive[Int, Int, Int, Option, Function0])
checkAll("Distributive[Nested[Function1[Int,?], Function0, ?]]", SerializableTests.serializable(Distributive[Nested[Function1[Int,?], Function0, ?]]))
}
}
5 changes: 5 additions & 0 deletions tests/src/test/scala/cats/tests/Tuple2KSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ class Tuple2KSuite extends CatsSuite {
checkAll("Tuple2K[ListWrapper, ListWrapper, Int]", PartialOrderTests[Tuple2K[ListWrapper, ListWrapper, Int]].partialOrder)
}

{
checkAll("Tuple2K[Function0, Function0, ?]", DistributiveTests[Tuple2K[Function0, Function0, ?]].distributive[Int, Int, Int, Option, Function0])
checkAll("Distributive[Tuple2K[Function0, Function0, ?]]", SerializableTests.serializable(Distributive[Tuple2K[Function0, Function0, ?]]))
}

test("show") {
forAll { (l1: Option[Int], l2: Option[Int]) =>
val tuple = Tuple2K(l1, l2)
Expand Down