Reputation: 1640
I have this version of try-with-resources
in Scala. I wonder if it is possible to make a generic version of this using Shapeless and HList?
import scala.util.{Failure, Success, Try}
class Loan1[A <: AutoCloseable](resource: A) {
def to[B](block: A => B): B = {
Try(block(resource)) match {
case Success(result) =>
resource.close()
result
case Failure(e) =>
resource.close()
throw e
}
}
}
class Loan2[A <: AutoCloseable, B <: AutoCloseable](r1: A, r2: B){
def to[R](block: (A,B) => R): R = {
Try(block(r1,r2)) match {
case Success(result) =>
r1.close(); r2.close()
result
case Failure(e) =>
r1.close(); r2.close()
throw e
}
}
}
object Loan {
def apply[A <: AutoCloseable](resource: A): Loan1[A] = new Loan1(resource)
def apply[A <: AutoCloseable, B <: AutoCloseable] (r1: A, r2: B)= new Loan2(r1, r2)
}
Something with the similar signature, I guess
def apply[L <: HList](list: L)(implicit con: LUBConstraint[L, AutoCloseable]) = ???
One more problem is how to make elements available in form of a tuple in block: (A,B) => R
section?
Is this possible to implement?
Upvotes: 5
Views: 1596
Reputation: 15086
It's actually not that hard. You need a way to get an HList
from a tuple (Generic.Aux[Tup, L]
) and a way to get a List[AutoClosable]
from the Hlist
(ToList[L, AutoCloseable]
).
There are probably other ways to do this than the ToList
part, but it's an easy fusion of the LUBConstraint[L, AutoCloseable]
and the requirement of being able to call close()
on every resource.
scala> :paste
// Entering paste mode (ctrl-D to finish)
import shapeless._, ops.hlist._
import scala.util.{Failure, Success, Try}
class Loan[Tup, L <: HList](resources: Tup)(
implicit
gen: Generic.Aux[Tup, L],
con: ToList[L, AutoCloseable]
) {
def to[B](block: Tup => B): B = {
Try(block(resources)) match {
case Success(result) =>
gen.to(resources).toList.foreach { _.close() }
result
case Failure(e) =>
gen.to(resources).toList.foreach { _.close() }
throw e
}
}
}
object Loan {
def apply[Tup, L <: HList](resources: Tup)(
implicit
gen: Generic.Aux[Tup, L],
con: ToList[L, AutoCloseable]
) = new Loan(resources)
}
// Exiting paste mode, now interpreting.
scala> class Bar() extends AutoCloseable { def close = println("close Bar"); def IAmBar = println("doing bar stuff") }
defined class Bar
scala> class Foo() extends AutoCloseable { def close = println("close Foo"); def IAmFoo = println("doing foo stuff") }
defined class Foo
scala> Loan(new Foo, new Bar).to{ case (f, b) => f.IAmFoo; b.IAmBar }
doing foo stuff
doing bar stuff
close Foo
close Bar
The only problem is that for the case of exactly 1 resource you need to write Tuple1(new Foo)
and pattern match like case Tuple1(f)
. The easiest solution is to keep the Loan1
part and replace the Loan2
part with a LoanN
that is implemented with shapeless and works for every arity >1. So that's almost equal to copy pasting my solution into yours and renaming my Loan
class to LoanN
:
import shapeless._, ops.hlist._, ops.nat._
import scala.util.{Failure, Success, Try}
class LoanN[Tup, L <: HList](resources: Tup)(
implicit
gen: Generic.Aux[Tup, L],
con: ToList[L, AutoCloseable]
) {
def to[B](block: Tup => B): B = {
Try(block(resources)) match {
case Success(result) =>
gen.to(resources).toList.foreach { _.close() }
result
case Failure(e) =>
gen.to(resources).toList.foreach { _.close() }
throw e
}
}
}
class Loan1[A <: AutoCloseable](resource: A) {
def to[B](block: A => B): B = {
Try(block(resource)) match {
case Success(result) =>
resource.close()
result
case Failure(e) =>
resource.close()
throw e
}
}
}
object Loan {
def apply[A <: AutoCloseable](resource: A): Loan1[A] = new Loan1(resource)
def apply[Tup, L <: HList, Len <: Nat](resources: Tup)(
implicit
gen: Generic.Aux[Tup, L],
con: ToList[L, AutoCloseable],
length: Length.Aux[L, Len],
gt: GT[Len, nat._1]
) = new LoanN(resources)
}
I also added the constraint that the length of the input has to be greater than 1. Otherwise there is a loophole where you pass in a case class Baz()
which can be converted to a List[Nothing]
which is a subtype of List[AutoClosable]
.
Undoubtedly the extra boilerplate with the Loan1
stuff could still be eliminated by writing a more complex typeclass yourself that is able to make a distinction between a single argument and a tuple of arguments.
You proposed to accept an HList
as argument and transform it to a tuple. That is also possible, with shapeless.ops.hlist.Tupler
. Then of course the users of that API will have to construct the HList
themselves, and you still have the problem of scala not having the pretty syntax for unwrapping a Tuple1
. That second problem can be solved with a really simple custom typeclass that unwraps a Tuple1[A]
to an A
and leaves everything else untouched:
sealed trait Unwrap[In] {
type Out
def apply(in: In): Out
}
object Unwrap extends DefaultUnwrap {
type Aux[In, Out0] = Unwrap[In] { type Out = Out0 }
def apply[T](implicit unwrap: Unwrap[T]): Unwrap.Aux[T, unwrap.Out] = unwrap
implicit def unwrapTuple1[A]: Unwrap.Aux[Tuple1[A], A] = new Unwrap[Tuple1[A]] {
type Out = A
def apply(in: Tuple1[A]) = in._1
}
}
trait DefaultUnwrap {
implicit def dontUnwrapOthers[A]: Unwrap.Aux[A, A] = new Unwrap[A] {
type Out = A
def apply(in: A) = in
}
}
Combine that with Tupler
and you have a relatively simple solution:
scala> :paste
// Entering paste mode (ctrl-D to finish)
import shapeless._, ops.hlist._
import scala.util.{Failure, Success, Try}
class LoanN[Tup, L <: HList, Res](resources: L)(
implicit
tupler: Tupler.Aux[L, Tup],
con: ToList[L, AutoCloseable],
unwrap: Unwrap.Aux[Tup, Res]
) {
def to[B](block: Res => B): B = {
Try(block(unwrap(tupler(resources)))) match {
case Success(result) =>
resources.toList.foreach { _.close() }
result
case Failure(e) =>
resources.toList.foreach { _.close() }
throw e
}
}
}
object Loan {
def apply[Tup, L <: HList, Res](resources: L)(
implicit
tupler: Tupler.Aux[L, Tup],
con: ToList[L, AutoCloseable],
unwrap: Unwrap.Aux[Tup, Res]
) = new LoanN(resources)
}
// Exiting paste mode, now interpreting.
scala> Loan(new Foo :: new Bar :: HNil).to{ case (f,b) => f.IAmFoo; b.IAmBar }
doing foo stuff
doing bar stuff
close Foo
close Bar
scala> Loan(new Foo :: HNil).to{ case (f) => f.IAmFoo }
doing foo stuff
close Foo
Upvotes: 4