Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert Shapeless HList to a Tuple

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?

like image 776
stanislav.chetvertkov Avatar asked Oct 18 '22 17:10

stanislav.chetvertkov


1 Answers

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
like image 74
Jasper-M Avatar answered Nov 15 '22 10:11

Jasper-M