Skip to content
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 scala.util.Using, for automatic resource management #6907

Merged
merged 7 commits into from Aug 15, 2018

Conversation

NthPortal
Copy link
Contributor

@NthPortal NthPortal commented Jul 9, 2018

Resolves scala/bug#11003
Resolves scala/bug#8028 (I think)
(and possibly others)

@NthPortal NthPortal added the WIP label Jul 9, 2018
@scala-jenkins scala-jenkins added this to the 2.13.0-M5 milestone Jul 9, 2018
@NthPortal
Copy link
Contributor Author

bikeshedding welcome

@Ichoran
Copy link
Contributor

Ichoran commented Jul 9, 2018

I would think the signature should be [R: Resource, A](=>R)(R => A): Try[A]

@hrhino
Copy link
Member

hrhino commented Jul 9, 2018

See also discussion at #6347.

@NthPortal
Copy link
Contributor Author

@Ichoran I guess I have two comments on that

  • why => R? Is there an advantage to having its evaluation be deferred?
  • there are a few disadvantages to having it return a Try
    • it becomes impossible to not use a Try; on the other hand, as it is, you can just wrap the whole thing in a Try if you want that
    • it makes it uglier to nest/compose using blocks; if you have four resources, you need to create four Trys, which really isn't necessary for the computation

regarding the latter point, this is what the implementation of the four resource method would look like if it returns a Try

using(resource1) { r1 =>
  using(resource2) { r2 =>
    using(resource3) { r3 =>
      using(resource4) { r4 =>
        body(r1, r2, r3, r4)
      }.get
    }.get
  }.get
}

not horrible, but certainly uglier

@Ichoran
Copy link
Contributor

Ichoran commented Jul 9, 2018

You want Try because throwing exceptions all over the place is messy. Maybe you should have two versions, one that wraps the exceptions and one that doesn't, but the usual case should be to use the Try version since you can't muck that one up.

If you're going to have a Try variant, then you need => R so that resource creation exceptions are also handled cleanly.

And of course you ought to be able to

for {
  r1 <- using(resource1)
  r2 <- using(resource2)
  r3 <- using(resource3)
  r4 <- using(resource4)
} yield body(r1, r2, r3, r4)

which means the whole thing should be an applicative functor, or functor-like (I don't know what the non-identity version is, where map is (F[A], A => B) => G[B] and flatMap is (F[A], A =>G[B]) => G[B]) for a specific permitted G, e.g. Try[A] or Either[ResourceException, A].

@Ichoran
Copy link
Contributor

Ichoran commented Jul 9, 2018

Okay, this all works. I just have a stub implementation for the actual resource logic, but I envision it like this:

class Using[R](r: => R) {
  import scala.util.{Try, Success, Failure}
  def apply[A](f: R => A)(implicit ev: Using.Resource[R]): Try[A] = map[A](f)(ev)
  def map[A](f: R => A)(implicit ev: Using.Resource[R]): Try[A] = Try{ val myR = r; val ans = f(myR); ev.close(myR); ans }
  def flatMap[A](f: R => Try[A])(implicit ev: Using.Resource[R]): Try[A] = Try{ val myR = r; val ans = f(myR); ev.close(myR); ans }.flatten
}
object Using {
  trait Resource[R]{ def close(r: R): Unit }
  def apply[R](r: => R): Using[R] = new Using(r)
}

Then we have usage like so:

case class Id[A](value: A) {}
object Id { 
  implicit def resourcedId[A]: Using.Resource[Id[A]] = new Using.Resource[Id[A]] { def close(r: Id[A]) = () } 
} 

@ for { u <- Using(Id(2)); v <- Using(Id("salmon")) } yield v.value * u.value 
res2: scala.util.Try[String] = Success("salmonsalmon")

@ 
Using(Id(true)).flatMap{ bool => Using(Id('x')){ c => if (bool.value) c.value.toUpper else c }} 
res5: scala.util.Try[Any] = Success('X')

@Ichoran
Copy link
Contributor

Ichoran commented Jul 9, 2018

I'm not sure what the name of this not-exactly-a-Monad thing is, if it has one, but the usage is really nice. You just use your stuff, and the right thing happens, and exceptions are caught.

@NthPortal
Copy link
Contributor Author

@Ichoran my only concern is how fatal exceptions are handled. I personally think that even in the case of a fatal exception, we ought to attempt to close the resource before re-throwing the exception (especially if it's a NonLocalReturnControl). I will have to have a look at your implementation to see how it handles it.

I think it makes sense to have both, but with the Try one more prominently defined, as you suggest. If we're to have a Using class though, should the throwing methods be renamed? Or is there not too much of a worry of having them be called, since it would require importing the entire contents of object Using?

Also, I would be more inclined to put the implicit Resource in the constructor of a Using, rather than on each of the methods.

@smarter
Copy link
Member

smarter commented Jul 9, 2018

See also @densh idea of scoped implicit lifetimes

@NthPortal
Copy link
Contributor Author

@Ichoran pushed the monad-ish impl you suggested. I'm open to burying the raw methods a bit more to make them less obvious

@Ichoran
Copy link
Contributor

Ichoran commented Jul 9, 2018

@NthPortal - You can put the typeclasses on the constructor, but then you can't chain the applies. So it would have to be Using(file.open).map(_.slurp) not Using(file.open){ _.slurp } (assuming that there was an open method on file, and that you can slurp an OpenFile).

* @return the result of the operation, if neither the operation nor
* closing the resource throws
*/
def using[R: Resource, A](resource: R)(body: R => A): A = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think Using.using is good nomenclature. Maybe Using.direct or Using.unsafe or Using.unwrapped?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my only concern is that I'd prefer a method name that, if imported statically, still makes sense.

e.g. Using.direct(file.open) makes sense, but direct(file.open) doesn't really

Copy link
Contributor

@Ichoran Ichoran Jul 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withResource?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that more I play with it in my mind, the less happy I am with the name withResource. something about it doesn't flow right.

what do you think of just resource or tryWithResource? The former works well when not imported - Using.resource.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or another possibility: tryWith

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(for comparison)

Using.withResource(new FileInputStream("foo.txt")) { _ => () }
withResource(new FileInputStream("foo.txt")) { _ => () }

Using.resource(new FileInputStream("foo.txt")) { _ => () }
resource(new FileInputStream("foo.txt")) { _ => () }

Using.tryWithResource(new FileInputStream("foo.txt")) { _ => () }
tryWithResource(new FileInputStream("foo.txt")) { _ => () }

Using.tryWith(new FileInputStream("foo.txt")) { _ => () }
tryWith(new FileInputStream("foo.txt")) { _ => () }

@NthPortal NthPortal changed the title [WIP] Add utility for automatic resource management [WIP] Add utility for automatic resource management [ci: last-only] Jul 9, 2018
@NthPortal NthPortal removed the WIP label Jul 14, 2018
@NthPortal
Copy link
Contributor Author

I think the implementation is pretty much done at this point. The scaladoc could use some improvement, which I would appreciate input on. The implementation is ready for review.

@NthPortal
Copy link
Contributor Author

should this be in scala.util.control (as it is currently) or in scala.util where Try is?

@NthPortal NthPortal changed the title [WIP] Add utility for automatic resource management [ci: last-only] Add utility for automatic resource management Jul 14, 2018
@NthPortal NthPortal changed the title Add utility for automatic resource management Add utility for automatic resource management [ci: last-only] Jul 14, 2018
@NthPortal
Copy link
Contributor Author

review @kmizu

@NthPortal NthPortal changed the title Add utility for automatic resource management [ci: last-only] Add utility for automatic resource management Jul 24, 2018
* @param f the operation to perform
* @param r an implicit [[Using.Resource]]
* @tparam A the return type of the operation
* @throws java.lang.IllegalStateException if the resource has already been used
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am open to being convinced that the resource can be generated again using the by-name parameter, and that the Using instance can be used arbitrarily many times.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NthPortal Sorry. I forgot to review.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NthPortal It looks good to me as far as I can see

@dwijnand
Copy link
Member

dwijnand commented Jul 27, 2018

should this be in scala.util.control (as it is currently) or in scala.util where Try is?

JFYI I noticed it's declared in scala.util.control but defined in scala/util/Using.scala.

@NthPortal
Copy link
Contributor Author

Good catch!

I expected IntelliJ to automatically change the package statement when I moved the files, but apparently it didn't ¯\_(ツ)_/¯

I'll fix that

Add Using utility to perform automatic resource management.
@NthPortal
Copy link
Contributor Author

@joshlemer bonus side effect of simplifying the implicit Resource for AutoClosable!

@joshlemer
Copy link
Member

joshlemer commented Aug 9, 2018

I am just throwing out a suggestion but maybe a factory method

Resource.fromTry[A](f: A => Try[_]): Resource[A]

might come frequently in handy as well

EDIT: Whoops fixed typo in the code

@NthPortal
Copy link
Contributor Author

@joshlemer It's not immediately apparent to me what that does

@joshlemer
Copy link
Member

@NthPortal something like

def fromTry[A](f: A => Try[_]): Resource[A] = (a: A) => f(a).failed.foreach(throw _)

@NthPortal
Copy link
Contributor Author

@joshlemer what's the value of catching exceptions in a Try just to rethrow them immediately?

@joshlemer
Copy link
Member

@NthPortal I was thinking more in the case where a user has a library which returns a Try for some IO operation, rather than itself throwing. In that case, the user would have to implement a Resource[T] which just throws any failed Try's, so it might be handy to have that case covered.

@NthPortal
Copy link
Contributor Author

@joshlemer aha, I see. I'm not super into it, but I'd love to hear others' thoughts on it

Copy link
Member

@lrytz lrytz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice, thanks! Asking for a few scaladoc improvements.

* operation using resources, after which it will release the resources, in reverse order
* of their creation. The resource opening, operation, and resource releasing are wrapped
* in a `Try`.
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the doc should explain what a resource is, how the Resource type class is used, what the user needs to do for non-AutoClosables.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there are two ways of using Using, I think this comment (which is the entry-point) should mention both, say why they exist, what the differences are.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you say two ways, to you mean Using(foo){ _.bar } vs for (f <- Using(foo)) yield f.bar, or Using(foo){ _.bar } vs Using.resource(foo){ _.bar }?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant Using(foo) vs Using.resource(foo), but explaining the possibilities how to use the monadic version is also welcome.

}

/** @define recommendUsing It is highly recommended to use the `Using` construct,
* which safely wraps resource usage and management in a `Try`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's maybe not clear to everyone what the Using construct is

src/library/scala/util/Using.scala Show resolved Hide resolved
@joshlemer
Copy link
Member

@NthPortal is there a reason not to make Using covariant?

@NthPortal
Copy link
Contributor Author

@joshlemer not that I can think of, but I also can't think of a reason why you'd ever declare it as a parameter to something, so I don't think it makes a difference. If you want to use its result for something, just use a Try.

@SethTisue
Copy link
Member

but won't add new things in RC1). @SethTisue, I defer to you as M5 lead, but I've put it back on the M5 radar for now

maybe it could still make M5, but I think it could wait for RC1 if it must. we aren't adding new things post-M5, but I think we should allow exceptions for new things that were already PR'ed well in advance of the M5 deadline. especially something like this that isn't being used yet and has zero effect on code that doesn't use it.

@NthPortal
Copy link
Contributor Author

@SethTisue I think that's a good idea. I don't know if I can push through the doc changes by tomorrow while also trying to get LazyList done

@SethTisue SethTisue added the release-notes worth highlighting in next release notes label Aug 12, 2018
* val lines: Try[List[String]] = Using(resource1) { r1 =>
* r1.lines.toList
* }
* }}}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ichoran what are your thoughts on removing the apply method, so that there aren't two ways to use it - you have to use a for comprehension (or manual map/flatMap)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the added benefit of allowing us to pass the implicit Resource on construction

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The for-comprehension-only version probably ought to be called Use or Used, not Using, if there's only that option. I generally prefer the apply version, but I don't have a strong preference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly about it either; I'm just concerned that some others might.

I'm going to leave it as is for now

@SethTisue
Copy link
Member

shall we merge as-is, to get feedback from a broader group, and figure there's still time to tweak it for RC1?

@lrytz
Copy link
Member

lrytz commented Aug 15, 2018

That works for me!

@SethTisue SethTisue merged commit 05f9d9b into scala:2.13.x Aug 15, 2018
@NthPortal
Copy link
Contributor Author

(this probably should have been squashed first, oops)

@SethTisue
Copy link
Member

doh, my bad!

@SethTisue SethTisue changed the title Add utility for automatic resource management Add scala.util.Using, for automatic resource management Aug 22, 2018
@NthPortal NthPortal deleted the bug#11003/PR branch September 2, 2018 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes worth highlighting in next release notes
Projects
None yet