-
Notifications
You must be signed in to change notification settings - Fork 534
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
Move uncancelable down to Bracket. Add laws specifying interaction. #241
Conversation
* }}} | ||
*/ | ||
def uncancelable[A](task: F[A]): F[A] = | ||
bracket(task)(pure)(_ => unit) |
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.
due to this law:
https://github.com/typelevel/cats-effect/pull/241/files#diff-321005aff53c79253fc4a54b188dc3b4R30hh
And the monad law f.flatMap(pure(_)) == f
, isn't this the same as identity?
What am I missing?
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.
Clicking on the link gets me nowhere :)
You're missing the interaction with data types that support cancellation, as they need to ensure that acquire and release cannot be cancelled
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.
N.B. this is in the model that supports auto-cancelable flatMaps.
Basically if you have auto-cancelable flatMaps, there's no way to implement bracket
without an acquire
that's not uncancelable
. It wouldn't work.
Consider that when acquiring a resource, you might do multiple flatMaps, but then the user cancels the tasks before bracket
has a chance to install the finalizers. It's harder to end up in such a situation without auto-cancelable binds because as a user you can reason about how acquire
behaves.
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.
@alexandru are auto-cancelable binds required to trigger odd behavior? I thought it would be enough if acquire
part contains a cancel boundary.
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, auto-cancelable binds are required. Example:
IO(new FileReader(file)).bracket { f =>
IO(f.read())
} { f =>
IO(f.close())
}
You can reason about this code and you don't need to ensure that acquire
is uncancelable because it already is. You can see that it is.
And in case acquire
is cancelable, it's usually something that's safe to cancel. Stupid sample, but you can picture:
val acquire = IO.cancelable { cb =>
val fh = new FileReader(file)
ec.execute(() => cb(Right(f)))
IO(fh.close())
}
// And then we don't care about the cancelability of acquire:
acquire.bracket(use)(release)
This mental model completely breaks down with auto-cancelable binds. Which is what I was ranting about in #192.
It starts from the implementation actually. It's not only desirable for acquire
to be uncancelable
. Rather, it's impossible to implement otherwise 😉
* F.uncancelable { F.bracketCase(F.unit)(_ => fa) { case Cancelled(_) => action; case _ => action2 } } <-> F.ensuring(fa)(action2) | ||
* }}} | ||
*/ | ||
def uncancelable[A](task: F[A]): 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.
Nit: I think we usually call these parameters fa
. Makes them a bit less concrete.
* A special case of [[bracket]], which is not meant for resource acquisition | ||
*/ | ||
def ensuring[A](task: F[A])(finalizer: F[Unit]): F[A] = | ||
bracket(unit)(_ => task)(_ => finalizer) |
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've written this a couple times.
}) <-> F.uncancelable(F.ensuring(fa)(onFinish)) | ||
|
||
def acquireAndReleaseAreUncancelable[A, B](fa: F[A], use: A => F[B], release: A => F[Unit]) = | ||
F.bracket(F.uncancelable(fa))(use)(a => F.uncancelable(release(a))) <-> F.bracket(fa)(use)(release) |
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.
Should we have canceled F[A] in our arbitraries to make this a more interesting law?
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.
Canceled F[A]
s cannot be made uncancelable :) We need F[A]
s that might get concurrently cancelled or not, which is a plenty of work that probably should be done later.
* _ <- fiber.cancel | ||
* _ <- fiber.join | ||
* } yield { | ||
* println("Tick!") |
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.
Maybe it should be _ <- F.delay(println("Tick!"))
? Just to avoid bad practices in docs ;)
@@ -38,6 +38,21 @@ trait BracketLaws[F[_], E] extends MonadErrorLaws[F, E] { | |||
|
|||
def bracketIsDerivedFromBracketCase[A, B](fa: F[A], use: A => F[B], release: A => F[Unit]) = | |||
F.bracket(fa)(use)(release) <-> F.bracketCase(fa)(use)((a, _) => release(a)) | |||
|
|||
def uncancelableIsDerivedFromBracket[A, B](fa: F[A]) = | |||
F.uncancelable(fa) <-> F.bracket(fa)(F.pure)(_ => F.unit) |
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.
This law is useful for us to read, but it's not useful in terms of tests via ScalaCheck.
We need something stronger in ConcurrentLaws
. Don't we have a law for uncancelable
in ConcurrentLaws
? We need one for bracket's acquire and maybe release too.
But make sure it works with Monix 😛
* renamed `task` to `fa` in signature * Stronger law on Concurrent * Made doc more pure :)
Codecov Report
@@ Coverage Diff @@
## master #241 +/- ##
==========================================
+ Coverage 88.73% 89.07% +0.33%
==========================================
Files 58 58
Lines 1492 1520 +28
Branches 145 157 +12
==========================================
+ Hits 1324 1354 +30
+ Misses 168 166 -2 |
some current laws: // law 0
F.bracket(fa)(use)(release) <-> F.bracketCase(fa)(use)((a, _) => release(a))
// law 1
F.bracketCase(fa)(f)((_, _) => F.unit) <-> F.flatMap(fa)(f)
// law 2
F.uncancelable(fa) <-> F.bracket(fa)(F.pure)(_ => F.unit) recall the monad law: So putting this together: // apply law 0 to the left hand side of this equation
F.bracket(fa)(F.pure)(_ => F.unit) <-> F.bracketCase(fa)(F.pure)((_, _) => F.unit)
// apply law1 to the right hand side of above.
F.bracket(fa)(F.pure)(_ => F.unit) <-> F.flatMap(fa)(F.pure)
// apply the monad bind identity, to right hand side of above.
F.bracket(fa)(F.pure)(_ => F.unit) <-> fa
// apply law2 to the left hand side of above.
F.uncancelable(fa) <-> fa Did I make a mistake? All lawful instances have to be identity according to these laws if I didn't. |
Law 1 doesn't look right to me. In presence of cancelable binds and concurrent cancellation - which we don't do a great job of testing against - it can't hold. |
@johnynek yeah, it needs to be restated, since now acquire has additional semantics. Thanks for noticing 👍 |
@oleg-py What a waste 🙂 That said, can you think of any sample for which Right now all I can think of is that on usage of |
Or in other words, whenever there's a call like: F.bracket(F.unit)(_ => task)(_ => release) I'm getting the feeling that all such calls are either not valid under auto-cancelable binds or very error prone (e.g. requiring idempotency on evaluating |
Good point about the name. I'll change it to Besides, stdlib has its own My use-case for it is not for resource handling, but for doing effecty stuff like logging or sending a message once the task has successfully completed. Previously I did That said, it's rare enough that I can live without, but it's a convenience I'd like to have baked in (the whole thing that attracted me in FP in the first place was having tons of such generic conveniences) |
It’s fine, we can have it 👍 and guarantee sounds good. |
Note we call this `onFinalize` in fs2.
… On May 25, 2018, at 6:22 AM, Alexandru Nedelcu ***@***.***> wrote:
It’s fine, we can have it 👍 and guarantee sounds good.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or mute the thread.
|
val lh = | ||
for { | ||
mVar <- F.liftIO(MVar[IO].of(b1)) | ||
task = F.bracket(F.unit)(_ => F.unit)(_ => F.liftIO(mVar.put(b2))) |
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 use
in this case should probably be F.never
to prevent release
from going off without a fiber.cancel
.
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.
LGTM
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.
Ah, wait, so you did not change IO's implementation?
If the laws aren't failing our IO, we have a problem somewhere.
See implementation: https://github.com/typelevel/cats-effect/blob/master/core/shared/src/main/scala/cats/effect/internals/IOBracket.scala#L33
def acquireIsNotCancelable[A](a1: A, a2: A) = { | ||
val lh = | ||
for { | ||
mVar <- F.liftIO(MVar[IO].of(a1)) |
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.
Seeing this MVar[IO]
used via F.liftIO
makes me uncomfortable. Why are we doing that, instead of working with MVar[F]
directly?
task = F.bracket(F.liftIO(mVar.put(a2)))(_ => F.never[A])(_ => F.unit) | ||
fiber <- F.start(task) | ||
_ <- fiber.cancel | ||
_ <- F.liftIO(ioTimer.shift) |
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 haven't paid attention to the shift
PR, I guess that's when the ioTimer
happened, but why do we have an ioTimer
in ConcurrentLaws
?
If anything, we need a Timer[F]
. If a Timer[IO]
is needed to test F[_]
, that kind of smells badly.
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.
We use Deferred[IO, ?]
to test cancelable
builder. Building that Deferred
requires Concurrent[IO]
, which now requires Timer[IO]
. And there's no way to get Timer[IO]
from Timer[F]
in Concurrent
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.
Deferred
works with just Async
. Do we really need a cancelable Deferred
in the cancelable
builder?
Seeing that Timer[IO]
in there seems odd given we are testing F[_]
.
@alexandru seems we're using |
Looking good 👍 |
A little cherry-pick from discussions in #192: since we require uninterruptible acquisition and cleanup, uncancelable can be described in terms of Bracket.
The rationale for pushing it to typeclass is two-fold:
Additionally, I added
ensuring
as a helper method I've found myself reinventing few times, and IIRC @alexandru mentioned it should probably be added :)I had to remove the
uncancelable
onConcurrent
(causes VerifyErrors for IO instance), so I moved the doc to the class level.There's also an oddity for Kleisli: looks like we assumed the function could be side-effecting, so we
suspend
ed theuncancelable
. Not sure what to do with that :)