Skip to content

Commit

Permalink
introduce NonEmptyAlternative, add laws and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
satorg committed Nov 22, 2021
1 parent bc7355e commit 6a30402
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 31 deletions.
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
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
}
11 changes: 6 additions & 5 deletions laws/src/main/scala/cats/laws/AlternativeLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ 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 {
Expand Down
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]
}
}
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,14 +27,11 @@ 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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cats
package laws
package discipline

import cats.laws.discipline.SemigroupalTests.Isomorphisms
import org.scalacheck.{Arbitrary, Cogen, Prop}
import Prop._

trait NonEmptyAlternativeTests[F[_]] extends ApplicativeTests[F] with SemigroupKTests[F] {
def laws: NonEmptyAlternativeLaws[F]

def nonEmptyAlternative[A: Arbitrary, B: Arbitrary, C: Arbitrary](implicit
ArbFA: Arbitrary[F[A]],
ArbFB: Arbitrary[F[B]],
ArbFC: Arbitrary[F[C]],
ArbFAtoB: Arbitrary[F[A => B]],
ArbFBtoC: Arbitrary[F[B => C]],
CogenA: Cogen[A],
CogenB: Cogen[B],
CogenC: Cogen[C],
EqFA: Eq[F[A]],
EqFB: Eq[F[B]],
EqFC: Eq[F[C]],
EqFABC: Eq[F[(A, B, C)]],
iso: Isomorphisms[F]
): RuleSet =
new RuleSet {
val name: String = "nonEmptyAlternative"
val bases: Seq[(String, RuleSet)] = Nil
val parents: Seq[RuleSet] = Seq(semigroupK[A], applicative[A, B, C])
val props: Seq[(String, Prop)] = Seq(
"left distributivity" -> forAll(laws.nonEmptyAlternativeLeftDistributivity[A, B] _),
"right distributivity" -> forAll(laws.nonEmptyAlternativeRightDistributivity[A, B] _),
"prependK consistent with pure and combineK" ->
forAll(laws.nonEmptyAlternativePrependKConsitentWithPureAndCombineK[A] _),
"appendK consistent with pure and combineK" ->
forAll(laws.nonEmptyAlternativeAppendKConsitentWithPureAndCombineK[A] _)
)
}
}

object NonEmptyAlternativeTests {
def apply[F[_]: NonEmptyAlternative]: NonEmptyAlternativeTests[F] =
new NonEmptyAlternativeTests[F] { def laws: NonEmptyAlternativeLaws[F] = NonEmptyAlternativeLaws[F] }

def composed[F[_]: NonEmptyAlternative, G[_]: Applicative]: NonEmptyAlternativeTests[λ[α => F[G[α]]]] =
new NonEmptyAlternativeTests[λ[α => F[G[α]]]] {
def laws: NonEmptyAlternativeLaws[λ[α => F[G[α]]]] = NonEmptyAlternativeLaws.composed[F, G]
}
}
Loading

0 comments on commit 6a30402

Please sign in to comment.