-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
WIP: Selective #3709
base: main
Are you sure you want to change the base?
WIP: Selective #3709
Changes from 43 commits
444e3bd
da7f756
d220349
4c7f87e
14d3e42
f554862
b7112ac
b8dc63b
7fd409b
254bf7f
ed71f06
7767906
466354b
f31020a
4a2b0be
955a03b
e35a097
df699d1
a50da48
387ab75
cb51502
06abe62
9b001dd
19b67b4
662cf5a
936847f
3372948
a866a20
260a219
a0f8709
37594b9
b56130d
9079e16
020a9fb
92f262f
762dd7d
691144e
3e31609
6b496ec
acce38c
7c78d54
c4c1442
3bcb791
dba3dc0
7a4d66a
6616707
bd0d4d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -257,6 +257,30 @@ trait Apply[F[_]] extends Functor[F] with InvariantSemigroupal[F] with ApplyArit | |
def ite(b: Boolean)(ifTrue: A, ifFalse: A) = if (b) ifTrue else ifFalse | ||
ap2(map(fcond)(ite))(ifTrue, ifFalse) | ||
} | ||
|
||
def select[A, B](fab: F[Either[A, B]])(ff: => F[A => B]): F[B] = | ||
selectA(fab)(ff) | ||
|
||
def selectA[A, B](fab: F[Either[A, B]])(ff: F[A => B]): F[B] = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remind me again why we need both select and selectA if setting them equal is lawful? Why not just add select which may or may not be override to be rigid? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I can live without There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect ifA without rigidity is a recipe for exponential blow ups in cost. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I have barely skimmed the whole conversation, so I do apologise for the lack of context, but I do think Personally I see Selective as just pulling things down from Monad, to get you more static analysis, so in an ideal world There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, I caused a bit of the wild goose chase here I think. One thing I was and am concerned about is a lawful implementation of Selective using Applicative, why not add to Applicative? So, I pushed to lower things to Applicative or Apply. But that means the default implementation winds up executing both sides, but as you note, you would almost never want that. It seems weird to open the door for exactly one use case that we can name (over approximating dependencies in a modestly dynamic graph). So, now my thinking has gotten to either:
So, one idea would be: // Comment that this is a rarely used type that is generally used to make static analysis of
// F[_] values that model a deferred computation.
trait Selective[F[_]] extends Applicative[F] {
def select[A, B](e: F[Either[A, B]])(fn: F[A => B]): F[B]
}
trait RigidSelective[F[_]] extends Selective[F] {
def ifM... // all the monad methods that can be implemented in terms of select
}
trait Monad[F[_]] extends RigidSelective[F] {
...
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great summary Ross. To simplify, I think ZipList, NonEmptyList.ZipNonEmptyList, Validated, IO.Par and Nested are the core things to think about. Clearly if Zip*List works, the other collections which are isomorphic should work too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need to cook now, but quick response to "So, one idea would be:..."
I have some qualms pulling the
Haskell gets around all these problems with an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oof.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
One very good reason for having a specific |
||
map2(fab, ff) { | ||
case (Left(a), f) => f(a) | ||
case (Right(b), _) => b | ||
} | ||
|
||
def branch[A, B, C](fab: F[Either[A, B]])(fl: => F[A => C])(fr: => F[B => C]): F[C] = { | ||
val innerLhs: F[Either[A, Either[B, C]]] = map(fab)(_.map(Left(_))) | ||
def innerRhs: F[A => Either[B, C]] = map(fl)(_.andThen(Right(_))) | ||
val lhs = select(innerLhs)(innerRhs) | ||
select(lhs)(fr) | ||
} | ||
|
||
@noop | ||
def ifS[A](fCond: F[Boolean])(fTrue: => F[A])(fFalse: => F[A]): F[A] = { | ||
val condition: F[Either[Unit, Unit]] = map(fCond)(if (_) EitherUtil.leftUnit else EitherUtil.unit) | ||
def left: F[Unit => A] = map(fTrue)(Function.const) | ||
def right: F[Unit => A] = map(fFalse)(Function.const) | ||
branch(condition)(left)(right) | ||
} | ||
} | ||
|
||
object Apply { | ||
|
@@ -289,12 +313,11 @@ object Apply { | |
object ops { | ||
implicit def toAllApplyOps[F[_], A](target: F[A])(implicit tc: Apply[F]): AllOps[F, A] { | ||
type TypeClassType = Apply[F] | ||
} = | ||
new AllOps[F, A] { | ||
type TypeClassType = Apply[F] | ||
val self: F[A] = target | ||
val typeClassInstance: TypeClassType = tc | ||
} | ||
} = new AllOps[F, A] { | ||
type TypeClassType = Apply[F] | ||
val self: F[A] = target | ||
val typeClassInstance: TypeClassType = tc | ||
} | ||
} | ||
trait Ops[F[_], A] extends Serializable { | ||
type TypeClassType <: Apply[F] | ||
|
@@ -312,19 +335,24 @@ object Apply { | |
typeClassInstance.ap2[B, C, D](self.asInstanceOf[F[(B, C) => D]])(fa, fb) | ||
def map2[B, C](fb: F[B])(f: (A, B) => C): F[C] = typeClassInstance.map2[A, B, C](self, fb)(f) | ||
def map2Eval[B, C](fb: Eval[F[B]])(f: (A, B) => C): Eval[F[C]] = typeClassInstance.map2Eval[A, B, C](self, fb)(f) | ||
def select[B, C](ff: => F[B => C])(implicit ev$1: A <:< Either[B, C]): F[C] = | ||
typeClassInstance.select[B, C](self.asInstanceOf[F[Either[B, C]]])(ff) | ||
def selectA[B, C](ff: F[B => C])(implicit ev$1: A <:< Either[B, C]): F[C] = | ||
typeClassInstance.selectA[B, C](self.asInstanceOf[F[Either[B, C]]])(ff) | ||
def branch[B, C, D](fl: => F[B => D])(fr: => F[C => D])(implicit ev$1: A <:< Either[B, C]): F[D] = | ||
typeClassInstance.branch[B, C, D](self.asInstanceOf[F[Either[B, C]]])(fl)(fr) | ||
} | ||
trait AllOps[F[_], A] extends Ops[F, A] with Functor.AllOps[F, A] with InvariantSemigroupal.AllOps[F, A] { | ||
type TypeClassType <: Apply[F] | ||
} | ||
trait ToApplyOps extends Serializable { | ||
implicit def toApplyOps[F[_], A](target: F[A])(implicit tc: Apply[F]): Ops[F, A] { | ||
type TypeClassType = Apply[F] | ||
} = | ||
new Ops[F, A] { | ||
type TypeClassType = Apply[F] | ||
val self: F[A] = target | ||
val typeClassInstance: TypeClassType = tc | ||
} | ||
} = new Ops[F, A] { | ||
type TypeClassType = Apply[F] | ||
val self: F[A] = target | ||
val typeClassInstance: TypeClassType = tc | ||
} | ||
} | ||
@deprecated("Use cats.syntax object imports", "2.2.0") | ||
object nonInheritedOps extends ToApplyOps | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package cats | ||
|
||
/** | ||
* Convenience methods and values for Either. | ||
*/ | ||
private[cats] object EitherUtil { | ||
def leftCast[A, B, C](right: Right[A, B]): Either[C, B] = | ||
right.asInstanceOf[Either[C, B]] | ||
def rightCast[A, B, C](left: Left[A, B]): Either[A, C] = | ||
left.asInstanceOf[Either[A, C]] | ||
|
||
val unit = Right(()) | ||
val leftUnit = Left(()) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package cats | ||
|
||
import simulacrum.typeclass | ||
import scala.annotation.implicitNotFound | ||
|
||
@implicitNotFound("Could not find an instance of RigidSelective for ${F}") | ||
@typeclass trait RigidSelective[F[_]] extends Applicative[F] | ||
|
||
object RigidSelective { | ||
/* ======================================================================== */ | ||
/* THE FOLLOWING CODE IS MANAGED BY SIMULACRUM; PLEASE DO NOT EDIT!!!! */ | ||
/* ======================================================================== */ | ||
|
||
/** | ||
* Summon an instance of [[RigidSelective]] for `F`. | ||
*/ | ||
@inline def apply[F[_]](implicit instance: RigidSelective[F]): RigidSelective[F] = instance | ||
|
||
@deprecated("Use cats.syntax object imports", "2.2.0") | ||
object ops { | ||
implicit def toAllRigidSelectiveOps[F[_], A](target: F[A])(implicit tc: RigidSelective[F]): AllOps[F, A] { | ||
type TypeClassType = RigidSelective[F] | ||
} = new AllOps[F, A] { | ||
type TypeClassType = RigidSelective[F] | ||
val self: F[A] = target | ||
val typeClassInstance: TypeClassType = tc | ||
} | ||
} | ||
trait Ops[F[_], A] extends Serializable { | ||
type TypeClassType <: RigidSelective[F] | ||
def self: F[A] | ||
val typeClassInstance: TypeClassType | ||
} | ||
trait AllOps[F[_], A] extends Ops[F, A] with Applicative.AllOps[F, A] { | ||
type TypeClassType <: RigidSelective[F] | ||
} | ||
trait ToRigidSelectiveOps extends Serializable { | ||
implicit def toRigidSelectiveOps[F[_], A](target: F[A])(implicit tc: RigidSelective[F]): Ops[F, A] { | ||
type TypeClassType = RigidSelective[F] | ||
} = new Ops[F, A] { | ||
type TypeClassType = RigidSelective[F] | ||
val self: F[A] = target | ||
val typeClassInstance: TypeClassType = tc | ||
} | ||
} | ||
@deprecated("Use cats.syntax object imports", "2.2.0") | ||
object nonInheritedOps extends ToRigidSelectiveOps | ||
|
||
/* ======================================================================== */ | ||
/* END OF SIMULACRUM-MANAGED CODE */ | ||
/* ======================================================================== */ | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -931,16 +931,21 @@ sealed abstract private[data] class ValidatedInstances extends ValidatedInstance | |
fab.leftMap(f) | ||
} | ||
|
||
implicit def catsDataApplicativeErrorForValidated[E](implicit E: Semigroup[E]): ApplicativeError[Validated[E, *], E] = | ||
new ValidatedApplicative[E] with ApplicativeError[Validated[E, *], E] { | ||
|
||
implicit def catsDataSelectiveErrorForValidated[E](implicit | ||
E: Semigroup[E] | ||
): ApplicativeError[Validated[E, *], E] = | ||
new ValidatedSelective[E] with ApplicativeError[Validated[E, *], E] { | ||
rossabaker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def handleErrorWith[A](fa: Validated[E, A])(f: E => Validated[E, A]): Validated[E, A] = | ||
fa match { | ||
case Validated.Invalid(e) => f(e) | ||
case v @ Validated.Valid(_) => v | ||
} | ||
def raiseError[A](e: E): Validated[E, A] = Validated.Invalid(e) | ||
} | ||
|
||
@deprecated("Use catsDataSelectiveErrorForValidated", "2.4.0") | ||
def catsDataApplicativeErrorForValidated[E](implicit E: Semigroup[E]): ApplicativeError[Validated[E, *], E] = | ||
catsDataSelectiveErrorForValidated | ||
} | ||
|
||
sealed abstract private[data] class ValidatedInstances1 extends ValidatedInstances2 { | ||
|
@@ -953,9 +958,13 @@ sealed abstract private[data] class ValidatedInstances1 extends ValidatedInstanc | |
def combine(x: Validated[A, B], y: Validated[A, B]): Validated[A, B] = x.combine(y) | ||
} | ||
|
||
implicit def catsDataCommutativeApplicativeForValidated[E: CommutativeSemigroup] | ||
implicit def catsDataCommutativeSelectiveForValidated[E: CommutativeSemigroup] | ||
: CommutativeApplicative[Validated[E, *]] = | ||
new ValidatedApplicative[E] with CommutativeApplicative[Validated[E, *]] | ||
new ValidatedSelective[E] with CommutativeApplicative[Validated[E, *]] | ||
|
||
@deprecated("Use catsDataCommutativeSelectiveForValidated", "2.4.0") | ||
def catsDataCommutativeApplicativeForValidated[E: CommutativeSemigroup]: CommutativeApplicative[Validated[E, *]] = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there's a CommutativeApplicative and a CommutativeMonad, we need to spend a few moments thinking about whether there's a CommutativeSelective. |
||
catsDataCommutativeApplicativeForValidated | ||
|
||
implicit def catsDataPartialOrderForValidated[A: PartialOrder, B: PartialOrder]: PartialOrder[Validated[A, B]] = | ||
new PartialOrder[Validated[A, B]] { | ||
|
@@ -1035,6 +1044,15 @@ sealed abstract private[data] class ValidatedInstances2 { | |
// scalastyle:off method.length | ||
} | ||
|
||
private[data] class ValidatedSelective[E: Semigroup] extends ValidatedApplicative[E] { | ||
override def select[A, B](fab: Validated[E, Either[A, B]])(ff: => Validated[E, A => B]): Validated[E, B] = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this not rigid? Looks like to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It skips the effect appropriately, but is inconsistent with This is consistent with override def select[A, B](fab: Validated[E, Either[A, B]])(ff: => Validated[E, A => B]): Validated[E, B] =
fab match {
case Valid(Right(b)) => Valid(b)
case Valid(Left(a)) => ff.map(_(a))
case e @ Invalid(e1) =>
ff match {
case Valid(_) => e
case Invalid(e2) => Invalid(EE.combine(e1, e2))
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's the We can regain associativity by not combining two val left: F[Either[A => B, B]] = ff.map(Left(_))
val right: F[(A => B) => B] = fa.map((a: A) => _(a))
left.select(right) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you ask me, associativity is a must. The law ap == apS is one I would at least consider relaxing. That law feels like it may preclude any rigid selects which are not also Monads. And if you are a monad you are back to the part where we aren't really adding something new. The struggle here feels like:
I'm not sure if I've seen an existence proof of something matching all three. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Several are close, but none hit the mark:
We may have conflated two concepts with what the paper calls rigid and our rigid skip laws, but everything that satisfies both seems to have a |
||
fab match { | ||
case Valid(Left(a)) => ff.map(_(a)) | ||
case Valid(Right(b)) => Valid(b) | ||
case i @ Invalid(_) => i | ||
} | ||
} | ||
|
||
private[data] class ValidatedApplicative[E: Semigroup] extends CommutativeApplicative[Validated[E, *]] { | ||
override def map[A, B](fa: Validated[E, A])(f: A => B): Validated[E, B] = | ||
fa.map(f) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whenA
takes anF[A]
and voids it.whenS
should probably do the same.