Scala 3 - Extract Tuple of wrappers and InverseMap on First Order Type

949 views Asked by At

I am trying to create a function, which takes a tuple of higher-kinded types and applies a function to the types within the higher-kinded types.

In the example below, there is a trait Get[A] which is our higher-kinded type. There is also a tuple of Get's: (Get[String],Get[Int]) as well as function from (String,Int) => Person.

Scala-3 has a Match-Type called InverseMap which converts the type (Get[String], Get[Int]) into what is essentially the type (String,Int).

So the ultimate goal is to write a function which can take a tuple with any number of Get[_] types and a function whose input matches the InserveMap types and finally return a Get[_], where the wrapped type is the result of the function.

I have attempted to create a function called genericF below to show the desired behavior, though it may not be correct -- but I think it does at least show the proper intent.

  case class Person(name: String, age: Int)
  trait Get[A] {
    def get: A
  }
  case class Put[A](get: A) extends Get[A]
    
  val t: (Get[String], Get[Int]) = (Put("Bob"), Put(42))
  
  val fPerson: (String,Int) => Person = Person.apply _
  
  def genericF[T<:Tuple,I<:Tuple.InverseMap[T,Get],B](f: I => B, t: T): Get[B] = ???
  val person: Get[Person] = genericF(fPerson, t)

I have set up a Scastie here: https://scastie.scala-lang.org/OleTraveler/QIyNHPLHQIKPv0lgsYbujA/23

1

There are 1 answers

9
username On BEST ANSWER

Your code is almost compiling already - the only thing is that fPerson is of type (String, Int) => Person instead of ((String, Int)) => Person (taking a tuple instead of 2 separate parameters).

The solution below this one is not nice, although it is perhaps more efficient for TupleXXL's. Here's a nicer version with typeclasses (Scastie):

val fPerson: ((String, Int)) => Person = Person.apply _

opaque type Extract[GT <: Tuple, RT <: Tuple] = GT => RT
given Extract[EmptyTuple, EmptyTuple] = Predef.identity
given [A, PG <: Tuple, PR <: Tuple](using p: Extract[PG, PR])
   as Extract[Get[A] *: PG, A *: PR] = {
  case h *: t => h.get *: p(t)
}

def genericF[GT <: Tuple, RT <: Tuple, B](
    f: RT => B,
    t: GT
)(using extract: Extract[GT, RT]): Get[B] = Put(f(extract(t)))

Here's one way you could implement genericF using Tuple.InverseMap (note that I switched the two parameters to genericF:

val fPerson: ((String, Int)) => Person = Person.apply _

type ExtractG = [G] =>> G match {
  case Get[a] => a
}

type AllGs[T <: Tuple] = T match {
  case EmptyTuple => DummyImplicit
  case Get[_] *: t => AllGs[t]
  case _ => Nothing
}

def extract[T <: Tuple](t: T)(using AllGs[T]): Tuple.InverseMap[T, Get] =
  t.map {
    [G] => (g: G) => g.asInstanceOf[Get[_]].get.asInstanceOf[ExtractG[G]]
  }.asInstanceOf[Tuple.InverseMap[T, Get]]

def genericF[B](
    t: Tuple,
    f: Tuple.InverseMap[t.type, Get] => B
)(using AllGs[t.type]): Get[B] = Put(f(extract(t)))

val person: Get[Person] = genericF(t, fPerson)

ExtractG is to make the PolyFunction compile, because it requires you apply a type constructor to its type parameter.

AllGs is to verify that the tuple consists only of Gets, because as pointed out by Dmytro Mitin, it isn't typesafe otherwise. If it's all Gets, the type becomes DummyImplicit, which Scala provides for us. Otherwise, it's Nothing. I guess it could conflict with other implicit/given Nothings in scope, but if you do have one already, you're screwed anyways.

Note that this will work only when you have Get and will need some modification if you also want it to work for tuples like (Put[String], GetSubclass[Int]).


Travis Stevens, the OP, has managed to get the solution above this one to work without creating AllGs, by using IsMappedBy. This is what they got (Scastie):

val fPerson: ((String, Int)) => Person = Person.apply _

type ExtractG = [G] =>> G match {
  case Get[a] => a
}

def extract[T <: Tuple, I <: Tuple.InverseMap[T, Get]](
    t: T
  )(using Tuple.IsMappedBy[Get][T]): I =
  t.map {
    [G] => (g: G) => g.asInstanceOf[Get[_]].get.asInstanceOf[ExtractG[G]]
  }.asInstanceOf[I]

def genericF[T <: Tuple, I <: Tuple.InverseMap[T, Get], B](
    t: T,
    f: I => B
)(using Tuple.IsMappedBy[Get][T]): Get[B] = Put(f(extract(t)))

And here's one using dependent types, just for fun (Scastie):

type Extract[T <: Tuple] <: Tuple = T match {
  case EmptyTuple => EmptyTuple
  case Get[a] *: t => a *: Extract[t]
}
 
type AllGs[T <: Tuple] = T match {
  case EmptyTuple => DummyImplicit
  case Get[_] *: t => AllGs[t]
  case _ => Nothing
}

def genericF[T <: Tuple : AllGs, B](
    t: T,
    f: Extract[t.type] => B
): Get[B] = {
  def extract[T <: Tuple](t: T): Extract[T] = t match {
    case _: EmptyTuple => EmptyTuple
    case (head *: tail): (Get[_] *: _) => head.get *: extract(tail)
  }
  Put(f(extract(t)))
}

I was hoping Extract wouldn't compile for tuples like (Put("foo"), 3), but unfortunately, AllGs is still necessary.