Scala3-在一阶类型上提取包装器和InverseMap的元组



我正在尝试创建一个函数,它接受一个更高级类型的元组,并将一个函数应用于更高级类型中的类型。

在下面的例子中,有一个trait Get[A],它是我们的高级类型。还有一个Get的元组:(Get[String],Get[Int])以及来自(String,Int) => Person的函数。

Scala-3有一个名为InverseMap的Match Type,它将类型(Get[String],Get[Int](转换为本质上的类型(String,Int(。

因此,最终目标是编写一个函数,该函数可以接受任何数量的Get[_]类型的元组,以及一个输入与InserveMap类型匹配的函数,并最终返回一个Get[_],其中封装的类型是函数的结果。

我试图在下面创建一个名为genericF的函数来显示所需的行为,尽管它可能不正确,但我认为它至少显示了正确的意图。

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)

我在这里设置了一个Scastie:https://scastie.scala-lang.org/OleTraveler/QIyNHPLHQIKPv0lgsYbujA/23

您的代码几乎已经在编译了——唯一的问题是fPerson的类型是(String, Int) => Person而不是((String, Int)) => Person(采用一个元组而不是两个单独的参数(。

下面的解决方案并不好,尽管它可能对TupleXXL更有效。这里有一个更好的版本与类型类(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)))

以下是使用Tuple.InverseMap实现genericF的一种方法(注意,我将两个参数切换到了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是为了编译PolyFunction,因为它需要将类型构造函数应用于其类型参数。

AllGs是为了验证元组仅由Gets组成,因为正如Dmytro-Mitin所指出的,否则它是不安全的。如果都是Gets,那么类型就变成了Scala为我们提供的DummyImplicit,否则就是Nothing。我想它可能与范围中其他隐含/给定的Nothing冲突,但如果您已经有了一个,那么无论如何都会失败。

请注意,只有当您有Get时,这才会起作用,如果您还希望它适用于像(Put[String], GetSubclass[Int])这样的元组,则需要进行一些修改。


Travis Stevens,OP,通过使用IsMappedBy,在不创建AllGs的情况下,成功地使上述解决方案发挥作用。这就是他们得到的(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)))

这里有一个使用依赖类型的,只是为了好玩(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)))
}

我希望Extract不会像(Put("foo"), 3)那样编译元组,但不幸的是,AllGs仍然是必要的。

最新更新