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
Add MVar #217
Add MVar #217
Conversation
Only had a very quick glance, but one thing: you need The original MVar in Haskell had this issue, and The other question is adding derived operations, specifically which ones should be added (e.g. |
Codecov Report
@@ Coverage Diff @@
## master #217 +/- ##
==========================================
+ Coverage 88.64% 88.66% +0.01%
==========================================
Files 54 57 +3
Lines 1251 1385 +134
Branches 100 135 +35
==========================================
+ Hits 1109 1228 +119
- Misses 142 157 +15 |
This is why I left Are there any
I would lean towards not including any derived operations. |
Well, the MVar implementation (the one in GHC) does, and it was deemed to be worth the cost due to the many problems implementing things that require reading.
I think this is an implementation detail that shouldn't influence the interface one way or another.
No strong opinions here, I can see pros and drawbacks of both |
@SystemFw OK, will try to add it |
@SystemFw so I did not remember |
Yeah that's what I meant, it proved too inconvenient/error prone in practice, and it was changed 👍 |
@SystemFw pushed a commit with an implementation that supports the atomic I have to add more tests and maybe clean it up a bit. |
* - [[take]] which empties the var if full, returning the contained | ||
* value, or blocks (asynchronously) otherwise until there is | ||
* a value to pull | ||
* |
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.
read
needs to be added here
I've added tests and documentation. It is now ready for review. |
@@ -41,10 +41,10 @@ private[effect] object Callback { | |||
} | |||
|
|||
/** Reusable `Right(())` reference. */ | |||
final val rightUnit = Right(()) |
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.
why are these not final now?
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.
AFAIK final val
in companion objects will make the compiler treat that as a constant and inline it. Which would defeat the point of declaring it in the first place, as we're doing such shenanigans in order to not allocate memory every time we need a Right(())
.
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.
@non I thought told me that was only if there is no type annotation, so we could add that but keep the final. But in any case probably not a big deal.
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.
Yes, final val x = ...
will inline but final val x: SomeType = ...
will not. It's a weird rule.
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.
Is there any difference between final val x: SomeType = ...
and val x: SomeType
?
From the JVM's point of view both will be final
, so I'd rather go with the later.
emptyRef.asInstanceOf[LinkedMap[K, V]] | ||
|
||
private val emptyRef = | ||
new LinkedMap[Any, Any](Map.empty, LongMap.empty, 0) |
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.
strictly speaking I think Nothing
would be a better type for the value.
private def unsafePut(a: A)(onPut: Listener[Unit]): Unit = { | ||
if (a == null) { | ||
onPut(Left(new NullPointerException("null not supported in MVarAsync/MVar"))) | ||
return |
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.
rather than using return
could we just wrap the rest in an else
?
Reviewers have to always think "is this a return that is actually ok or not?" if we don't
case current @ WaitForTake(value, puts) => | ||
val update = WaitForTake(value, puts.enqueue(a -> onPut)) | ||
if (!stateRef.compareAndSet(current, update)) { | ||
// $COVERAGE-OFF$ |
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 we need to do this? can we not try to trigger races that will actually hit this branch?
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.
Triggering race conditions that hit those branches is very difficult and you're not gaining much.
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.
It makes me nervous if we have concurrency primatives we aren’t testing in race conditions.
Seems like it shouldn’t be too hard to hit he branch: just have N threads and maybe two MVars that they take from one and put to the other or something like that.
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.
That's easier said than done. In Monix I've got plenty of tests like that and I hate writing them with all my heart 🙂 Note that the current tests are concurrent, but hitting all of those branches is hard an non-deterministic.
That said, I just removed the coverage annotations. So if others want to improve code coverage, they are free to do it.
F.delay(MVarAsync[F, A](a)(F)) | ||
|
||
/** Builds an empty `MVar`. */ | ||
def empty[A](implicit F: Async[F]): F[MVar[F, 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.
Why do you have a second Async
here? You already have one as a class parameter.
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.
Good catch. It's from my experiments 🙂
* | ||
* @see [[async]] | ||
*/ | ||
def apply[F[_]](implicit F: Concurrent[F]): ConcurrentBuilder[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.
TBH I would prefer to have a simple def apply[F[_]: Concurrent): MVar[F, A]
. I understand it brings some type inference help to scalac
but on the other hand is less clearer to read and it is not consistent with the other datatypes recently introduced (Ref
, Deferred
, Semaphore
and Resource
).
site/src/main/tut/datatypes/mvar.md
Outdated
until there is a value available, at which point the operation resorts | ||
to a `take` followed by a `put` | ||
|
||
An additional but non-atomic operation is `read`, which tries reading the |
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.
read
is now an atomic operation? :)
} | ||
``` | ||
|
||
## Use-case: Producer/Consumer Channel |
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 example~ 👍
@johnynek so regarding your review, I've added another concurrency test with
In my experience thus far with Cats's monad transformers and with Monix's Iterant, without partially applying types in
Besides I did make changes. So in addition to MVar[IO].empty[Int] <-> MVar.empty[IO, Int]
MVar[IO].init("hello") <-> MVar.init[IO, String]("hello")
MVar[IO].initF(IO("hello")) <-> MVar.initF(IO("Hello")) Also there's no more |
* | ||
* @see [[init]], [[initF]] and [[empty]] | ||
*/ | ||
def apply[F[_]](implicit F: Concurrent[F]): ApplyBuilders[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.
I like this pattern - can we use it in Ref
too? Specifically the notion of having both the regular method on companion and then ApplyBuilders
for the type curried type params.
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.
👍
- Ref.ApplyBuilders is introduced and made public, similar to typelevel#217 - Refs can be constructed as Ref[F, A].of(a) or Ref[F].of(a) - Unsafe builder is only available as a version with all type parameters
Adds MVar to Cats-Effect.
PR also contains website documentation.
Implementation and documentation are imported from Monix, with some changes for working with
F[_]
and for cancelability.