-
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
Add Bracket type class #113
Conversation
Obviously this is just a WIP so mine it's a discussion item and not an actual code review, but this is not the model of def getFile = bracket(openFile)(file => file.pure[IO])((file,_) => IO(file.close))
def uhOh = getFile.flaMap(f => IO(f.readFirstLine)) by the time we try reading, the file is closed already. P.S. Apologies if this is already perfectly clear to you, but I thought it would be good to specify it anyway for other readers/just in case |
Ah you're right, that's a problem indeed, didn't think about that. |
Another thing to keep in mind is that taking an |
@SystemFw wait, so let us be clear, is this something that we really want to work? def getFile = bracket(openFile)(file => file.pure[IO])((file,_) => IO(file.close))
def uhOh = getFile.flaMap(f => IO(f.readFirstLine)) How would that even work? When is Are you sure you're not thinking of FS2's |
@SystemFw My assumption on this, is that when So |
On an |
Just to be clear guys, in my mind val resource = acquire()
try {
use(resource)
} finally {
release(resource)
} If you have something else in mind, then we need to translate it in code like this. |
I don't see how without either linear types or returning |
Ah, you're right! Well, good to have that clarified early on :) |
Yep, got my wires crossed and was indeed thinking of that, I should probably change topic for today 😄 EDIT: fwiw, I want that |
|
||
trait Bracket[F[_], E] extends MonadError[F, E] { | ||
def bracket[A, B](acquire: F[A])(use: A => F[B]) | ||
(release: (A, Either[Throwable, B]) => F[Unit]): F[B] |
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.
A good start, just 2 things:
- minor mistake: you have an
E
parameter, so you can't useThrowable
anymore - we need to be forward thinking and allow cancellation behavior in
release
— e.g. Monix does not emit an error on cancellation, possibly diverging from other designs and the error type is not enough to distinguish between termination in error and cancellation anyway
I propose this:
trait Bracket[F[_], E] extends MonadError[F, E] {
def bracket[A, B](acquire: F[A])
(use: A => F[B])
(release: (A, Option[Either[E, B]]) => F[Unit]): F[B]
}
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.
You'll notice that the current cats.effect.IO
will never emit None
. Which is actually quite OK, given that this IO
can't be cancelled, seems fitting.
import cats.implicits._ | ||
import cats.laws._ | ||
|
||
trait BracketLaws[F[_], E] extends MonadErrorLaws[F, E] { |
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.
can you add a comment giving an example of why all MonadError
does not satify these laws.
etb <- eta.fold(IO.raiseError, use).attempt | ||
_ <- eta.fold(_ => IO.unit, a => release(a, etb)) | ||
b <- etb.fold(IO.raiseError, b => IO(b)) | ||
} yield b |
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.
in library code, I prefer to write things the most efficient way, Wonder about all the use of fold and allocation of closures here. What about:
for {
a <- this
etb <- use(a).attempt
_ <- release(a, etb)
b <- IO.fromEither(etb)
} yield b
Also note, IO.fromEither
would probably be better as:
def fromEither[A](e: Either[Throwable, A]): IO[A] =
e match {
case Right(a) => pure(a)
case Left(err) => raiseError(err)
}
and avoid the allocations of the closures.
@LukaJCB I'm very happy about this proposal, however it needs to distinguish cancellation, or lets say for now the notion of non-termination: def bracket[A, B](acquire: F[A])
(use: A => F[B])
(release: (A, Option[Either[E, B]]) => F[Unit]): F[B] Otherwise it's not very useful for me and I now need it for |
@alexandru
Since you also mention non-termination here :) |
@SystemFw I don't have the energy atm, I'd like to postpone that one, because I'm arguing for async abstractions that have been in Java and .NET and against Haskell and that's not a good place to be in 😀 IMO receiving a Either way, the ability to distinguish between cancellation and normal termination is a must that needs to be in the signature. If the signature is now too complicated, we can add helpers as well |
Yeah I sensed that, that's why I added "when you have time" 😄
Yep 👍 |
@alexandru I agree, that failure and cancellation should be separate, but didn't implement it yet, because I'm not sure if an option of either is the best encoding of that and when or if we come up with something different I don't have to change all of the code :) For now I have no better suggestion than what you suggested, however :D |
I am perhaps missing something, but how you would get |
@pchlupacek you don't, you can either get |
@pchlupacek You don't, you'd get a I was thinking just now, maybe it should be three different functions? def bracket[A, B](acquire: F[A])
(use: A => F[B])
(releaseOnError: (A, E) => F[Unit])
(releaseOnSuccess: (A, B) => F[Unit])
(releaseOnCancel: A => F[Unit]): F[B] WDYT? Doesn't have to be curried in this way btw. |
I am still a confused here, bracket's result is |
So we have 3 choices:
If you want to accommodate all possible variations, then do number 3. |
OK, so the problem with different functions is that you don't understand from the signature that only one of those 3 functions will get called, whereas if we do a single function then it's pretty clear due to the input. |
Also the signature assumes you may cancel |
That's not a problem because if val resource = acquire()
try {
use(resource)
} finally {
release(resource)
} Obviously you can't put |
yeh, I am on board with that, hence you just release the resource when it is acquired. Agree when it fails, it fails. Having said that how you plan to produce |
@LukaJCB no worries, I'm going to push that in another PR, along with added documentation for the microsite. |
Well, it's already done 😄 Thanks again! |
Ah, OK, cool. Will follow up with another PR with some changes anyway 😜 |
…into LukaJCB-add-bracket2
So CI looks good and I think this PR is mergeable now, anyone care for a re-review? @rossabaker @pchlupacek @SystemFw |
Bracket changes: rename bracketE, add microsite docs
Well, I also contributed to it, so I can't approve it 😄 One thing to consider is that this is a blocker for the scheduled 1.0.0-RC. After this gets merged, we'll release a hash version first, to ensure that the laws are passing in Monix as well — laws tend to be written assuming |
site/src/main/tut/datatypes/io.md
Outdated
### bracketCase | ||
|
||
The `bracketCase` operation is the generalized `bracket`, also receiving | ||
an `ExitCode` in `release` in order to distinguish between: |
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.
-> ExitCase
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.
Thank you, fixed!
* (via `MonadError[F, E]`) | ||
* - [[ExitCase$.Canceled Canceled]]: for abortion | ||
*/ | ||
sealed abstract class ExitCase[+E] |
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.
Damn, this needs to implement Serializable
at least, maybe Product
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.
On it!
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.
Given that "bracket" is already in extensive and incompatible use in the streaming types (e.g., fs2.Stream
. monix.tail.Iterant
), I think we might consider a documentation about why this bracket is not that bracket. I don't think it needs to block this merge, but I fully anticipate it being an FAQ.
* (via `MonadError[F, E]`) | ||
* - [[ExitCase$.Canceled Canceled]]: for abortion | ||
*/ | ||
sealed abstract class ExitCase[+E] extends Serializable |
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 imagine we want Product
here, too?
@rossabaker you're right, we'll need a FAQ of some sort. Well, I don't view the Can it happen after the merge? |
Yes, I don't want the FAQ to delay the merge. I tried to think of a nice alternate name for either this bracket or Stream/Iterant style bracket, and failed. They both seem pretty well established under this name. |
Some WIP code I came up with, at some point the future, this might fix #88.