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

[proposal] optimize Alternative (part 1): introduce NonEmptyAlternative with prependK and appendK methods #4014

Merged
merged 3 commits into from
Nov 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 15 additions & 18 deletions core/src/main/scala/cats/Alternative.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import simulacrum.typeclass
import scala.annotation.implicitNotFound

@implicitNotFound("Could not find an instance of Alternative for ${F}")
@typeclass trait Alternative[F[_]] extends Applicative[F] with MonoidK[F] { self =>
@typeclass trait Alternative[F[_]] extends NonEmptyAlternative[F] with MonoidK[F] { self =>

/**
* Fold over the inner structure to combine all of the values with
Expand All @@ -23,7 +23,7 @@ import scala.annotation.implicitNotFound
*/
def unite[G[_], A](fga: F[G[A]])(implicit FM: Monad[F], G: Foldable[G]): F[A] =
FM.flatMap(fga) { ga =>
G.foldLeft(ga, empty[A])((acc, a) => combineK(acc, pure(a)))
G.foldLeft(ga, empty[A])((acc, a) => appendK(acc, a))
}

/**
Expand Down Expand Up @@ -61,8 +61,8 @@ import scala.annotation.implicitNotFound
def separateFoldable[G[_, _], A, B](fgab: F[G[A, B]])(implicit G: Bifoldable[G], FF: Foldable[F]): (F[A], F[B]) =
FF.foldLeft(fgab, (empty[A], empty[B])) { case (mamb, gab) =>
G.bifoldLeft(gab, mamb)(
(t, a) => (combineK(t._1, pure(a)), t._2),
(t, b) => (t._1, combineK(t._2, pure(b)))
(t, a) => (appendK(t._1, a), t._2),
(t, b) => (t._1, appendK(t._2, b))
)
}

Expand Down Expand Up @@ -104,12 +104,11 @@ object Alternative {
object ops {
implicit def toAllAlternativeOps[F[_], A](target: F[A])(implicit tc: Alternative[F]): AllOps[F, A] {
type TypeClassType = Alternative[F]
} =
new AllOps[F, A] {
type TypeClassType = Alternative[F]
val self: F[A] = target
val typeClassInstance: TypeClassType = tc
}
} = new AllOps[F, A] {
type TypeClassType = Alternative[F]
val self: F[A] = target
val typeClassInstance: TypeClassType = tc
}
}
trait Ops[F[_], A] extends Serializable {
type TypeClassType <: Alternative[F]
Expand All @@ -122,24 +121,22 @@ object Alternative {
def separateFoldable[G[_, _], B, C](implicit ev$1: A <:< G[B, C], G: Bifoldable[G], FF: Foldable[F]): (F[B], F[C]) =
typeClassInstance.separateFoldable[G, B, C](self.asInstanceOf[F[G[B, C]]])(G, FF)
}
trait AllOps[F[_], A] extends Ops[F, A] with Applicative.AllOps[F, A] with MonoidK.AllOps[F, A] {
trait AllOps[F[_], A] extends Ops[F, A] with NonEmptyAlternative.AllOps[F, A] with MonoidK.AllOps[F, A] {
type TypeClassType <: Alternative[F]
}
trait ToAlternativeOps extends Serializable {
implicit def toAlternativeOps[F[_], A](target: F[A])(implicit tc: Alternative[F]): Ops[F, A] {
type TypeClassType = Alternative[F]
} =
new Ops[F, A] {
type TypeClassType = Alternative[F]
val self: F[A] = target
val typeClassInstance: TypeClassType = tc
}
} = new Ops[F, A] {
type TypeClassType = Alternative[F]
val self: F[A] = target
val typeClassInstance: TypeClassType = tc
}
}
@deprecated("Use cats.syntax object imports", "2.2.0")
object nonInheritedOps extends ToAlternativeOps

/* ======================================================================== */
/* END OF SIMULACRUM-MANAGED CODE */
/* ======================================================================== */

}
15 changes: 14 additions & 1 deletion core/src/main/scala/cats/Composed.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,24 @@ private[cats] trait ComposedMonoidK[F[_], G[_]] extends MonoidK[λ[α => F[G[α]
override def empty[A]: F[G[A]] = F.empty
}

private[cats] trait ComposedNonEmptyAlternative[F[_], G[_]]
extends NonEmptyAlternative[λ[α => F[G[α]]]]
with ComposedApplicative[F, G]
with ComposedSemigroupK[F, G] { outer =>

def F: NonEmptyAlternative[F]
}

private[cats] trait ComposedAlternative[F[_], G[_]]
extends Alternative[λ[α => F[G[α]]]]
with ComposedApplicative[F, G]
with ComposedNonEmptyAlternative[F, G]
with ComposedMonoidK[F, G] { outer =>

def F: Alternative[F]

override def prependK[A](a: A, fa: F[G[A]]): F[G[A]] = F.prependK(G.pure(a), fa)

override def appendK[A](fa: F[G[A]], a: A): F[G[A]] = F.appendK(fa, G.pure(a))
}

private[cats] trait ComposedFoldable[F[_], G[_]] extends Foldable[λ[α => F[G[α]]]] { outer =>
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/cats/Monad.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import scala.annotation.implicitNotFound
ifM(p)(
ifTrue = {
map(b.value) { bv =>
Left(G.combineK(xs, G.pure(bv)))
Left(G.appendK(xs, bv))
}
},
ifFalse = pure(Right(xs))
Expand Down Expand Up @@ -66,7 +66,7 @@ import scala.annotation.implicitNotFound
*/
def untilM[G[_], A](f: F[A])(cond: => F[Boolean])(implicit G: Alternative[G]): F[G[A]] = {
val p = Eval.later(cond)
flatMap(f)(x => map(whileM(map(p.value)(!_))(f))(xs => G.combineK(G.pure(x), xs)))
flatMap(f)(x => map(whileM(map(p.value)(!_))(f))(xs => G.prependK(x, xs)))
}

/**
Expand Down
84 changes: 84 additions & 0 deletions core/src/main/scala/cats/NonEmptyAlternative.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package cats

import simulacrum.typeclass
import scala.annotation.implicitNotFound

@implicitNotFound("Could not find an instance of NonEmptyAlternative for ${F}")
@typeclass trait NonEmptyAlternative[F[_]] extends Applicative[F] with SemigroupK[F] { self =>

/**
* Lift `a` into `F[_]` and prepend it to `fa`.
*
* Example:
* {{{
* scala> NonEmptyAlternative[List].prependK(1, List(2, 3, 4))
* res0: List[Int] = List(1, 2, 3, 4)
* }}}
*/
def prependK[A](a: A, fa: F[A]): F[A] = combineK(pure(a), fa)

/**
* Lift `a` into `F[_]` and append it to `fa`.
*
* Example:
* {{{
* scala> NonEmptyAlternative[List].appendK(List(1, 2, 3), 4)
* res0: List[Int] = List(1, 2, 3, 4)
* }}}
*/
def appendK[A](fa: F[A], a: A): F[A] = combineK(fa, pure(a))

override def compose[G[_]: Applicative]: NonEmptyAlternative[λ[α => F[G[α]]]] =
new ComposedNonEmptyAlternative[F, G] {
val F = self
val G = Applicative[G]
}
}

object NonEmptyAlternative {
/* ======================================================================== */
/* THE FOLLOWING CODE IS MANAGED BY SIMULACRUM; PLEASE DO NOT EDIT!!!! */
/* ======================================================================== */

/**
* Summon an instance of [[NonEmptyAlternative]] for `F`.
*/
@inline def apply[F[_]](implicit instance: NonEmptyAlternative[F]): NonEmptyAlternative[F] = instance

@deprecated("Use cats.syntax object imports", "2.2.0")
object ops {
implicit def toAllNonEmptyAlternativeOps[F[_], A](target: F[A])(implicit tc: NonEmptyAlternative[F]): AllOps[F, A] {
type TypeClassType = NonEmptyAlternative[F]
} = new AllOps[F, A] {
type TypeClassType = NonEmptyAlternative[F]
val self: F[A] = target
val typeClassInstance: TypeClassType = tc
}
}
trait Ops[F[_], A] extends Serializable {
type TypeClassType <: NonEmptyAlternative[F]
def self: F[A]
val typeClassInstance: TypeClassType
// Note: `prependK` has to be added manually since simulacrum is not able to handle `self` as a second parameter.
// def prependK(a: A): F[A] = typeClassInstance.prependK[A](a, self)
def appendK(a: A): F[A] = typeClassInstance.appendK[A](self, a)
}
trait AllOps[F[_], A] extends Ops[F, A] with Applicative.AllOps[F, A] with SemigroupK.AllOps[F, A] {
type TypeClassType <: NonEmptyAlternative[F]
}
trait ToNonEmptyAlternativeOps extends Serializable {
implicit def toNonEmptyAlternativeOps[F[_], A](target: F[A])(implicit tc: NonEmptyAlternative[F]): Ops[F, A] {
type TypeClassType = NonEmptyAlternative[F]
} = new Ops[F, A] {
type TypeClassType = NonEmptyAlternative[F]
val self: F[A] = target
val typeClassInstance: TypeClassType = tc
}
}
@deprecated("Use cats.syntax object imports", "2.2.0")
object nonInheritedOps extends ToNonEmptyAlternativeOps

/* ======================================================================== */
/* END OF SIMULACRUM-MANAGED CODE */
/* ======================================================================== */
}
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 @@ -14,6 +14,7 @@ abstract class AllSyntaxBinCompat

trait AllSyntax
extends AlternativeSyntax
with NonEmptyAlternativeSyntax
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure, where to insert this new trait. I'd prefer to maintain all mix-in traits in some particular order, but I cannot figure out any specific order here.

with AlignSyntax
with ApplicativeSyntax
with ApplicativeErrorSyntax
Expand Down
36 changes: 36 additions & 0 deletions core/src/main/scala/cats/syntax/nonEmptyAlternative.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cats
package syntax

trait NonEmptyAlternativeSyntax {
implicit final def catsSyntaxNonEmptyAlternative[F[_], A](fa: F[A]): NonEmptyAlternativeOps[F, A] =
new NonEmptyAlternativeOps(fa)
}

final class NonEmptyAlternativeOps[F[_], A] private[syntax] (private val fa: F[A]) extends AnyVal {

/**
* @see [[NonEmptyAlternative.prependK]]
*
* Example:
* {{{
* scala> import cats.syntax.all._
*
* scala> List(2, 3, 4).prependK(1)
* res0: List[Int] = List(1, 2, 3, 4)
* }}}
*/
def prependK(a: A)(implicit F: NonEmptyAlternative[F]): F[A] = F.prependK(a, fa)

/**
* @see [[NonEmptyAlternative.appendK]]
*
* Example:
* {{{
* scala> import cats.syntax.all._
*
* scala> List(1, 2, 3).appendK(4)
* res0: List[Int] = List(1, 2, 3, 4)
* }}}
*/
def appendK(a: A)(implicit F: NonEmptyAlternative[F]): F[A] = F.appendK(fa, a)
}
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 @@ -71,4 +71,5 @@ package object syntax {
object vector extends VectorSyntax
object writer extends WriterSyntax
object set extends SetSyntax
object nonEmptyAlternative extends NonEmptyAlternativeSyntax
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems that the initial intent was to keep all inner objects of object syntax just in alphabetic order by their names. But later the order was broken (perhaps due to an oversight). So I'm putting the new object to the end of all existing objects for now.

}
16 changes: 11 additions & 5 deletions laws/src/main/scala/cats/laws/AlternativeLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,28 @@ package laws

import cats.syntax.all._

trait AlternativeLaws[F[_]] extends ApplicativeLaws[F] with MonoidKLaws[F] {
trait AlternativeLaws[F[_]] extends NonEmptyAlternativeLaws[F] with MonoidKLaws[F] {
implicit override def F: Alternative[F]
implicit def algebra[A]: Monoid[F[A]] = F.algebra[A]
implicit override def algebra[A]: Monoid[F[A]] = F.algebra[A]

def alternativeRightAbsorption[A, B](ff: F[A => B]): IsEq[F[B]] =
(ff.ap(F.empty[A])) <-> F.empty[B]

// Perhaps should be deprecated in favor of nonEmptyAlternativeLeftDistributivity
def alternativeLeftDistributivity[A, B](fa: F[A], fa2: F[A], f: A => B): IsEq[F[B]] =
((fa |+| fa2).map(f)) <-> ((fa.map(f)) |+| (fa2.map(f)))
nonEmptyAlternativeLeftDistributivity[A, B](fa, fa2, f)

// Perhaps should be deprecated in favor of nonEmptyAlternativeRightDistributivity
def alternativeRightDistributivity[A, B](fa: F[A], ff: F[A => B], fg: F[A => B]): IsEq[F[B]] =
((ff |+| fg).ap(fa)) <-> ((ff.ap(fa)) |+| (fg.ap(fa)))

nonEmptyAlternativeRightDistributivity(fa, ff, fg)
}

object AlternativeLaws {
def apply[F[_]](implicit ev: Alternative[F]): AlternativeLaws[F] =
new AlternativeLaws[F] { def F: Alternative[F] = ev }

def composed[M[_], N[_]](implicit M: Alternative[M], N: Applicative[N]): AlternativeLaws[λ[α => M[N[α]]]] =
new AlternativeLaws[λ[α => M[N[α]]]] {
def F: Alternative[λ[α => M[N[α]]]] = M.compose[N]
}
Comment on lines +25 to +29
Copy link
Contributor Author

@satorg satorg Nov 19, 2021

Choose a reason for hiding this comment

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

Just a call-out: this introduces a new ability to validate laws for composed instances. This feature is totally new, it didn't exist before. I decided to add it because composed instances were not covered with any tests at all.

But currently only composed Alternative and NonEmptyAlternative instances (see below) are supported to keep the PR scope narrow.

}
34 changes: 34 additions & 0 deletions laws/src/main/scala/cats/laws/NonEmptyAlternativeLaws.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cats
package laws

import cats.syntax.all._

trait NonEmptyAlternativeLaws[F[_]] extends ApplicativeLaws[F] with SemigroupKLaws[F] {
implicit override def F: NonEmptyAlternative[F]
implicit def algebra[A]: Semigroup[F[A]] = F.algebra[A]

def nonEmptyAlternativeLeftDistributivity[A, B](fa: F[A], fa2: F[A], f: A => B): IsEq[F[B]] =
((fa |+| fa2).map(f)) <-> ((fa.map(f)) |+| (fa2.map(f)))

def nonEmptyAlternativeRightDistributivity[A, B](fa: F[A], ff: F[A => B], fg: F[A => B]): IsEq[F[B]] =
((ff |+| fg).ap(fa)) <-> ((ff.ap(fa)) |+| (fg.ap(fa)))

def nonEmptyAlternativePrependKConsitentWithPureAndCombineK[A](fa: F[A], a: A): IsEq[F[A]] =
fa.prependK(a) <-> (a.pure[F] <+> fa)

def nonEmptyAlternativeAppendKConsitentWithPureAndCombineK[A](fa: F[A], a: A): IsEq[F[A]] =
fa.appendK(a) <-> (fa <+> a.pure[F])
}

object NonEmptyAlternativeLaws {
def apply[F[_]](implicit ev: NonEmptyAlternative[F]): NonEmptyAlternativeLaws[F] =
new NonEmptyAlternativeLaws[F] { def F: NonEmptyAlternative[F] = ev }

def composed[M[_], N[_]](implicit
M: NonEmptyAlternative[M],
N: Applicative[N]
): NonEmptyAlternativeLaws[λ[α => M[N[α]]]] =
new NonEmptyAlternativeLaws[λ[α => M[N[α]]]] {
def F: NonEmptyAlternative[λ[α => M[N[α]]]] = M.compose[N]
}
}
12 changes: 7 additions & 5 deletions laws/src/main/scala/cats/laws/discipline/AlternativeTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import cats.laws.discipline.SemigroupalTests.Isomorphisms
import org.scalacheck.{Arbitrary, Cogen, Prop}
import Prop._

trait AlternativeTests[F[_]] extends ApplicativeTests[F] with MonoidKTests[F] {
trait AlternativeTests[F[_]] extends NonEmptyAlternativeTests[F] with MonoidKTests[F] {
def laws: AlternativeLaws[F]

def alternative[A: Arbitrary, B: Arbitrary, C: Arbitrary](implicit
Expand All @@ -27,17 +27,19 @@ trait AlternativeTests[F[_]] extends ApplicativeTests[F] with MonoidKTests[F] {
new RuleSet {
val name: String = "alternative"
val bases: Seq[(String, RuleSet)] = Nil
val parents: Seq[RuleSet] = Seq(monoidK[A], applicative[A, B, C])
val parents: Seq[RuleSet] = Seq(monoidK[A], nonEmptyAlternative[A, B, C])
val props: Seq[(String, Prop)] = Seq(
"left distributivity" -> forAll(laws.alternativeLeftDistributivity[A, B] _),
"right distributivity" -> forAll(laws.alternativeRightDistributivity[A, B] _),
"right absorption" -> forAll(laws.alternativeRightAbsorption[A, B] _)
)
}

}

object AlternativeTests {
def apply[F[_]: Alternative]: AlternativeTests[F] =
new AlternativeTests[F] { def laws: AlternativeLaws[F] = AlternativeLaws[F] }

def composed[F[_]: Alternative, G[_]: Applicative]: AlternativeTests[λ[α => F[G[α]]]] =
new AlternativeTests[λ[α => F[G[α]]]] {
def laws: AlternativeLaws[λ[α => F[G[α]]]] = AlternativeLaws.composed[F, G]
}
}
Loading