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
FreeC: make the F parameter covariant #1730
Conversation
a41fb15
to
968a50b
Compare
+F[_]
968a50b
to
633b06e
Compare
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.
Looks awesome. Some questions about source and binary compatibility. I'm okay with some limited source breakages in 2.2.0 as long as they are relatively infrequently hit by users. Binary breakages have to wait a while.
build.sbt
Outdated
@@ -205,6 +205,16 @@ lazy val mimaSettings = Seq( | |||
}.toSet | |||
}, | |||
mimaBinaryIssueFilters ++= Seq( | |||
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.Stream.PurePipeOps"), |
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.
To preserve binary compatibility, we should deprecate this stuff until 3.0 release -- I think that will coincide with cats-effect 3, probably in the 6+ month timeframe.
} | ||
|
||
object RepeatEvalSanityTest extends App { | ||
def id[A]: Pipe[Pure, A, A] = { | ||
def go(s: Stream[Pure, A]): Pull[Pure, A, Unit] = | ||
def id[F[_], A]: Pipe[F, A, A] = { |
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.
Did these have to change? If so, could you update the PR with a summary of public facing source compatibility API changes?
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.
So, at some point in writing these changes, I thought I may need to change it. It
Now that the F
is covariant and not invariant, I am not sure if there is still much use in keeping the idea of a Pipe
with a same parameter for the input and output stream. Ideally, you may want the pipe to be applied to any sub-F-type of the input, and be compatible with any super-F-type of the output (just like any function).
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.
F
in Stream
and Pull
have always been covariant though, so changing internals shouldn't cause changes to the API/usage.
@@ -161,8 +160,8 @@ object Pull extends PullLowPriority { | |||
* Returns a pull that evaluates the supplied by-name each time the pull is used, | |||
* allowing use of a mutable value in pull computations. | |||
*/ | |||
def suspend[F[x] >: Pure[x], O, R](p: => Pull[F, O, R]): Pull[F, O, R] = | |||
fromFreeC(FreeC.suspend(p.get)) | |||
def suspend[F[_], O, R](p: => Pull[F, O, R]): Pull[F, O, R] = |
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 F[x] >: Pure[x]
bound is to prevent inference of Nothing
. Any reason this changes here?
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.
Do you have an example code snippet where lack of this >: Pure[x]
was causing a problem?
In general, I would think that covariance of both Pull
and FreeC
would make locally inferring F[A] = Nothing
not a problem, since it can up-cast it naturally when used in another context.
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.
Right, but Stream
and Pull
were already covariant in F
. I can try to come up with a failing example later, but take a look at this blog post which documents why we had to create Pure
in the first place: https://mpilquist.github.io/blog/2018/07/04/fs2/. Basically, there are cases where inferring F = Nothing
causes scalac to do the wrong thing.
|
||
/** Creates a pure stream that emits the supplied values. To convert to an effectful stream, use `covary`. */ | ||
def apply[F[x] >: Pure[x], O](os: O*): Stream[F, O] = emits(os) | ||
def apply[O](os: O*): Stream[Pure, O] = emits(os) |
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 think this one has the potential to impact lots of folks, though maybe not -- perhaps folks typically use Stream(...).covary[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.
Note that, with a covariant F
, we can deprecate the covary
methods in favour of type-ascriptions, such as
Stream(1): Stream[IO, Int]
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'm 👎 on deprecating covary
, it's much more handy than type ascriptions (especially because you only need to type in the effect type and the element type is inferred).
I rarely use apply
with an explicit type though, so I'm okay with this particular change :)
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.
Yeah I much prefer covary[F]
over type ascription as I don't have to repeat the output type -- and it's worse with pull where you have to specify both output and result types.
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 undone the change to the apply
method. In general, I am undoing any change to the public API that is not strictily necessary to introduce the covariance in this Pull Request.
We change the definition of `FreeC`, and its several subclasses, to be covariant on the kind-1 type `F[_]` parameter. Since the F is covariant in FreeC, we can define the Result classes as "Pure" cases of FreeC, that is to say those where the `F` parameter is the `fs2.Pure` (an alias for Nothing). We can also remove the `asFreeC` type-casting method, as well as a few other "asInstanceOf" operations. Now that FreeC is Covariant, in Stream and Pull we can unify the outer type parameter `F` with the type parameter of the inner `FreeC`, so that a Pull (or a Stream) are now just simple wrappers. This allows to remove some internal uses of the `get` function. Whenever we need to add some type information (over Pure or INothing), we can just do that with plain Scala type annotation. - Remove some uses of asInstanceOf - Also, we can save some type annotations in the benchmarks. Remove some usages of the covary methods. - Remove commented lines inside tut. Apparently Scala 2.13 console does not deal with them. - TextSpec: issues involving pipes and variance - Add Mima problems
When compiling on Scala 2.12, we were hitting on a compiler bug, that manifests as a compiler crash, wherever we pattern-match on the Step and OpenScope sub-classes of FreeC. This bug is due to our FreeC now being a Generalised ADT, with a type parameter that is covariant and of higuer-kind. The specific characteristic why this fails for these two sub-classes and not others is that, in these, the type-parameter F appears in the fields of the case class. While this bug has been fixed in Scala 2.13, in fs2 we still need to adapt to work on Scala 2.12. To that end, we have to relax a bit the type-safety of these classes, and introduce some `asInstanceOf` to bridge the gaps left by that.
0a7ce71
to
2feb2d1
Compare
2455fdf
to
18a34d1
Compare
- In order to preserve source-compatibility, we avoid removing the extra F[_] type parameters from Stream/Pull factory methods. - Restore the "PurePipeOps" that had been deleted at first. - We can drop the "Mima" filters, so our changes no longer modify the build.
18a34d1
to
0dc2946
Compare
- Undo changes to MemorySanityChecks.scala that may not be needed. - Remove more uses of the `fromFreeC` and `get` methods in Stream/Pull.
6aa5a32
to
4308637
Compare
4308637
to
9c7bef2
Compare
I reverted a few other unnecessary changes in StreamSpec, CompilationTest, and the README. Merging on successful Travis build. Thanks for all the work on this one, it looks great! |
Follows on the early work on #1615.
In the internal implementation of streams, we change the definition of the
FreeC[F[_], +O, +R]
generalised abstract data type (GADT), to turn theF
into a covariant parameter of higuer-kind, that is,FreeC[ + F[_], +O, +R ]
.Pure subclasses: those subclasses of
FreeC
that do not involveF
, such as theResult
and theOutput
, can be specialised withF = Pure
(an alias forNothing
of theF
kind). We can then integrateResult
into theFreeC
GADT, and dropasFreeC
.The classes
Pull
andStream
no longer need to resort to theNothing
parameter and use type-casts. Instead, the type-parameters ofStream
andFreeC
are no aligned. As a consequence, we will no longer need to use thecovary
methods ofStream
orPull
.Broken edge for Scala 2.12.
When we made the first changes, we noticed that pattern-matching on the algebra classes
Step
andOpenScope
was causing a compiler crash. This problem has been fixed in Scala 2.13. However, in order to keep support for 2.12, we need to use a small hack for these two case classes:Step
, we have to replace an appearance ofF
byAny
, on the definition, and use some type-casts (asInstanceOf
) when using that field.OpenScope
, we use type parameterG[_]
for theConcurrent
instance, different to theF
from theFreeC
. Of course, we know from the rest of the program that theF
should be theG
.Changes for a subsequent Pull Request
To keep this Pull Request small, there are some pending changes to be done later on:
covary
,get
, andfromFreeC
methods inPull
andSstream
Pull
into a type alias (not just anAnyVal
wrapper) ofFreeC
, closer to the vision of thefs3
branch.