-
-
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 32 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 |
---|---|---|
@@ -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,59 @@ | ||
package cats | ||
|
||
import simulacrum.typeclass | ||
import scala.annotation.implicitNotFound | ||
|
||
@implicitNotFound("Could not find an instance of RigidSelective for ${F}") | ||
@typeclass trait RigidSelective[F[_]] extends Selective[F] { | ||
override def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] = { | ||
val left: F[Either[A => B, B]] = map(ff)(Left(_)) | ||
val right: F[(A => B) => B] = map(fa)((a: A) => _(a)) | ||
select(left)(right) | ||
} | ||
} | ||
|
||
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 Selective.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 |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package cats | ||
|
||
import simulacrum.{noop, typeclass} | ||
import scala.annotation.implicitNotFound | ||
|
||
@implicitNotFound("Could not find an instance of Selective for ${F}") | ||
@typeclass trait Selective[F[_]] extends Applicative[F] { | ||
rossabaker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def select[A, B](fab: F[Either[A, B]])(ff: => F[A => B]): F[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] = { | ||
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. this is the exact same signature as ifM. Why can't we use the same name? This is going to be really confusing. I would rather just name this method ifM. 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 this were a clean slate, we'd pull I would like to call it |
||
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) | ||
} | ||
|
||
@noop | ||
def whenS[A](fCond: F[Boolean])(fTrue: => F[Unit]): F[Unit] = | ||
ifS(fCond)(fTrue)(unit) | ||
} | ||
|
||
object Selective { | ||
/* ======================================================================== */ | ||
/* THE FOLLOWING CODE IS MANAGED BY SIMULACRUM; PLEASE DO NOT EDIT!!!! */ | ||
/* ======================================================================== */ | ||
|
||
/** | ||
* Summon an instance of [[Selective]] for `F`. | ||
*/ | ||
@inline def apply[F[_]](implicit instance: Selective[F]): Selective[F] = instance | ||
|
||
@deprecated("Use cats.syntax object imports", "2.2.0") | ||
object ops { | ||
implicit def toAllSelectiveOps[F[_], A](target: F[A])(implicit tc: Selective[F]): AllOps[F, A] { | ||
type TypeClassType = Selective[F] | ||
} = new AllOps[F, A] { | ||
type TypeClassType = Selective[F] | ||
val self: F[A] = target | ||
val typeClassInstance: TypeClassType = tc | ||
} | ||
} | ||
trait Ops[F[_], A] extends Serializable { | ||
type TypeClassType <: Selective[F] | ||
def self: F[A] | ||
val typeClassInstance: TypeClassType | ||
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 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 Applicative.AllOps[F, A] { | ||
type TypeClassType <: Selective[F] | ||
} | ||
trait ToSelectiveOps extends Serializable { | ||
implicit def toSelectiveOps[F[_], A](target: F[A])(implicit tc: Selective[F]): Ops[F, A] { | ||
type TypeClassType = Selective[F] | ||
} = new Ops[F, A] { | ||
type TypeClassType = Selective[F] | ||
val self: F[A] = target | ||
val typeClassInstance: TypeClassType = tc | ||
} | ||
} | ||
@deprecated("Use cats.syntax object imports", "2.2.0") | ||
object nonInheritedOps extends ToSelectiveOps | ||
|
||
/* ======================================================================== */ | ||
/* 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] | ||
): Selective[Validated[E, *]] with 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] | ||
: CommutativeApplicative[Validated[E, *]] = | ||
new ValidatedApplicative[E] with CommutativeApplicative[Validated[E, *]] | ||
implicit def catsDataCommutativeSelectiveForValidated[E: CommutativeSemigroup] | ||
: Selective[Validated[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] with Selective[Validated[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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
selectA
is likeifA
: requires 'Apply, not rigid even for
FlatMap`sselectM
(not added yet) is likeifM
: requiresFlatMap
, rigidselect
is likeifS
: requires 'Apply, rigid for
FlatMap`s, otherwise effect's choiceI can live without
selectA
andselectM
, but their existence parallels some other functions. (I'll note that @SystemFw just dumped onifA
on Gitter today.)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.
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 comment
The 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
ifA
is one of the worse things added in cats.An
if
that executes both branches is what you want 0.01% of the time, and users have been told to "use the least powerful constraint you can", which makes it a recipe for disaster. We can't even claim we didn't see this, because theif
+ macro interaction in sbt has been hated for the same reason for years.Personally I see Selective as just pulling things down from Monad, to get you more static analysis, so in an ideal world
ifA
would be gone (and similarly forselect*
on Apply),Selective
would called just that, and haveifM
(ifS
?), etc. So (barring misunderstanding of what I've skimmed), I think I'm largely in agreement with @johnynekThere 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.
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:
def select
to Monad and implement it with flatMap, add the select laws there (and require it to be rigid). This allows Monad instances to optimize select if they can (like Parser or Gen could).So, one idea would be:
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.
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 comment
The 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:..."
Apply
andApplicative
for now. We can circle back on maps and tuples in a second PR, or not, after we're done learning from this.Selective[F]
. Several of the laws (identity, associativity, distributivity) can stick there.I have some qualms pulling the
*M
functions down toRigidSelective
:ap == apS
law, or both.ifM
without being aRigidSelective
, but we'd have no typeclass for these. We could pull those functions down toSelective
, but the name would lose its current sense of rigidity.'*M
functions are onFlatMap
. Now we need to think about that relationship earlier than we'd like.Haskell gets around all these problems with an
*S
set of functions that's usually like*M
. Great for laws, confusing for new users autocompleting weird names.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.
Oof.
ZipList
is in the pile of things that areApply
but notApplicative
that I'm trying to forget about for now.ZipStream
,ZipLazyList
, andIO.Par
are allApplicative
.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.
The
Applicative[ZipLazyList]
laws don't terminate, so I don't know if ourSelective[ZipLazyList]
is lawful. But it appears to be Haskell-rigid: theApply
laws pass whenap
is implemented asapS
. It's not Cats-Issue-3709-rigid, because we have to evaluateff
even if we get a lazy stream of Rights.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.
One very good reason for having a specific
Selective
thing that isn't also a monad is precisely to leverage static structure with dynamic behaviours. Take parsers, for example:Applicative
isn't strong enough to do context-sensitive things,Monad
is, andSelective
gives you some (of the most common) context-sensitive behaviours but not all of them. This is important, because if you wanted to generate fast hand-written recursive descent for a parser combinator implementation, you need a fully static structure. That rules outMonad
, but notSelective
orApplicative
: luckily these can almost always be enough for your practical parsing needs, but such a library wouldn't be able to make aMonad
instance (see https://github.com/j-mie6/ParsleyHaskell as an example of such a library). Basically, one killer application is for staged DSLs, which can't be monadic without runtime code-generation.