Scala,通用元组

13

我有一个通用方法,可以接受任何大小的元组,唯一的约束是这个元组的第一个元素应该是 MyClass 类型。

类似于下面这样:

trait MyTrait[T <: (MyClass, _*)] {
  getMyClass(x: T): MyClass = x._1
}

我尝试过这个

trait MyTrait[T <: (MyClass, _) with (MyClass, _, _) with (MyClass, _, _) with ...] {
  getMyClass(x: T): MyClass = x._1
}

但是我遇到了错误 未绑定通配符类型

6个回答

11

如果你想避免使用样板代码或运行时反射,Shapeless 是最佳选择。你可以使用 IsComposite 类型类来对元组的第一个元素进行类型级约束:

import shapeless.ops.tuple.IsComposite

trait MustBeFirst

class MyClass[P <: Product](p: P)(implicit ev: IsComposite[P] { type H = MustBeFirst }) {
  def getMustBeFirst(x: P): MustBeFirst = ev.head(p)
}

然后:

scala> val good2 = (new MustBeFirst {}, "")
good2: (MustBeFirst, String) = ($anon$1@7294acee,"")

scala> val good3 = (new MustBeFirst {}, "", 123)
good3: (MustBeFirst, String, Int) = ($anon$1@6eff9288,"",123)

scala> val good4 = (new MustBeFirst {}, "", 'xyz, 123)
good4: (MustBeFirst, String, Symbol, Int) = ($anon$1@108cdf99,"",'xyz,123)

scala> val bad2 = ("abc", 123)
bad2: (String, Int) = (abc,123)

scala> new MyClass(good2)
res0: MyClass[(MustBeFirst, String)] = MyClass@5297aa76

scala> new MyClass(good3)
res1: MyClass[(MustBeFirst, String, Int)] = MyClass@3f501844

scala> new MyClass(good4)
res2: MyClass[(MustBeFirst, String, Symbol, Int)] = MyClass@24e15478

scala> new MyClass(bad2)
<console>:15: error: could not find implicit value for parameter ev: shapeless.ops.tuple.IsComposite[(String, Int)]{type H = MustBeFirst}
       new MyClass(bad2)
       ^

如果您需要使用一个trait,您可以将ev(代表"evidence")要求放在定义中而不是构造函数中:

trait MyTrait[P <: Product] {
  implicit def ev: IsComposite[P] { type H = MustBeFirst }
}

现在,任何实例化MyTrait的类都必须提供证据表明P是一个元组,并且MustBeFirst是其第一个元素。


可能有点挑剔,但如果我正确阅读了shapeless源代码,IsComposite也将证明元组大小至少为2。这可能是一个相关的细节,因为大小为1的元组,尽管很奇怪,但确实存在^^ - C4stor
1
@C4stor的IsComposite函数适用于带有Unit类型尾部的Tuple1 - Oleg Pyzhcov
1
虽然我喜欢这个解决方案和shapeless库,但我不知道如何在我的情况下使用它。 这是我的实际用例:https://pastebin.com/yCKs0eiQ - Gigitsu

5

虽然有一定的风险,但在这种情况下可以使用结构类型:

trait MyTrait {
  def getMyClass(x: {def _1: MyClass}): MyClass = x._1
}

2
这是Scala中的复合类型或“结构类型”,与“鸭子类型”不同,具有明确定义的含义。 - Tim
1
这个出乎意料的简单。 - sarveshseri
仅为强制约束,您可以使用 Product { def _1: MyClass },但它仍然可以接受具有 def _1 的 case class。 - Yuriy
似乎使用结构类型时,方法 _1 通过反射调用,这会导致性能下降。 - Gigitsu
@Gigitsu 我认为这是编译时反射,所以“性能差”应该是在编译而不是运行代码时。对吗? - sarveshseri
显示剩余2条评论

2

Scala无法使用未知大小的通用元组,因为产品不会自己继承。您可以尝试使用Shapeless或来自play json库的Products。


1
你能澄清一下这里的“不继承自身”是什么意思吗?我不明白它的含义,也不知道它如何有帮助。 - Travis Brown
嗨 :-) 提问者没有指定未知大小,而是任何大小,这更弱。例如,无形的HLists(正如您所建议的那样)绝对不是未知大小,因为它们的大小在编译时已知 :-)作为Travis Brown,我无法完全理解您在此答案中的意思,澄清会非常有帮助 :-) - C4stor

1
这在Scala 3中现在是可能的,非常简单明了:
class MyClass

trait MyTrait[T <: MyClass *: _] {
  def getMyClass(x: T): MyClass = x.head
}
*: 中缀运算符是类型级别相当于 +: 序列前置(或列表的 ::)。 因此,我们要求类型 T 是一个元组,其第一个成员的类型为 MyClass。 请注意,通常无法使用 _1_2 等属性来检索通用元组的成员,而应使用类似列表的方法(如 headtailapply 等)访问它们。 更令人惊讶的是,这些方法是类型安全的,因为它们携带精确的类型信息。

演示:

object Test1 extends MyTrait[(MyClass, Int, String)] // Compiles
//object Test2 extends MyTrait[(Int, String)] // Does not compile!
//object Test3 extends MyTrait[EmptyTuple] // Neither does this

val myClass = Test1.getMyClass((new MyClass, 1, "abc"))
summon[myClass.type <:< MyClass] // Compiles

了解更多关于匹配类型, 类型推断的信息。


0

你需要从 Product 继承你的 trait,通过这个 trait 你可以拥有 productIteratorproductArityproductElement 来处理返回值。下面是一个示例

case class MyClass()

trait MyTrait[T <: Product] {
  def getMyClass(x: T): Option[MyClass] = 
                if(
                     x.productIterator.hasNext 
                                   && 
                     x.productIterator.next().isInstanceOf[MyClass]
                 ){
    Some(x.productIterator.next().asInstanceOf[MyClass])
  } else {
    None
  }
}

case class Test() extends MyTrait[Product]

你可以这样调用

Test().getMyClass((MyClass(), 1,3,4,5))
//res1: Option[MyClass] = Some(MyClass())

Test().getMyClass((1,3,4,5))
//res2: Option[MyClass] = None

希望这能对你有所帮助。

-1
如果您正在寻找编译时保证,那么这是Shapeless的一个用例之一。
您需要在您的build.sbt中添加Shapeless。
libraryDependencies ++= Seq("
  com.chuusai" %% "shapeless" % "2.3.3"
)

现在,您可以使用Shapeless来定义一个类型安全的getter,它带有编译时的保证。
scala> import shapeless._
// import shapeless._

scala> import ops.tuple.IsComposite
// import ops.tuple.IsComposite

scala> import syntax.std.tuple._
// import syntax.std.tuple._

scala> case class Omg(omg: String)
// defined class Omg

scala> val myStringTuple = ("All is well", 42, "hope")
// myStringTuple: (String, Int, String) = (All is well,42,hope)

scala> val myOmgTuple = (Omg("All is well"), 42, "hope")
// myOmgTuple: (Omg, Int, String) = (Omg(All is well),42,hope)

现在,如果您想使用特定类型的“first” getter来丰富您的元组,则可以这样做:

scala> implicit class GetterForProduct[B <: Product](b: B) {
     |   def getFirst[A](implicit comp: IsComposite[B] { type H = A }): A = b.head
     | }
// defined class GetterForProduct

scala> val myString = myStringTuple.getFirst[String]
// myString: String = All is well

scala>   val myOmgError = myOmgTuple.getFirst[String]
// <console>:24: error: could not find implicit value for parameter comp: shapeless.ops.tuple.IsComposite[(Omg, Int, String)]{type H = String}
//         val myOmgError = myOmgTuple.getFirst[String]
//                                             ^

scala>   val myOmg = myOmgTuple.getFirst[Omg]
// myOmg: Omg = Omg(All is well

如果您不需要隐式增强,只是想要一种在getter中“锁定”类型并将其用于相应类型的方法,

scala> trait FirstGetterInProduct[A] {
     |   def getFirst[B <: Product](b: B)(implicit comp: IsComposite[B] { type H = A }): A = b.head
     | }
// defined trait FirstGetterInProduct

scala> object firstGetterInProductForString extends FirstGetterInProduct[String]
// defined object firstGetterInProductForString

scala> object firstGetterInProductForOmg extends FirstGetterInProduct[Omg]
// defined object firstGetterInProductForOmg

// Right tuple with right getter,
scala> val myString = firstGetterInProductForString.getFirst(myStringTuple)
// myString: String = All is well

// will fail at compile time for tuple with different type for first
scala> val myOmgError = firstGetterInProductForString.getFirst(myOmgTuple)
// <console>:23: error: could not find implicit value for parameter comp: shapeless.ops.tuple.IsComposite[(Omg, Int, String)]{type H = String}
//          val myOmgError = firstGetterInProductForString.getFirst(myOmgTuple)
//                                                                 ^

scala> val myOmg = firstGetterInProductForOmg.getFirst(myOmgTuple)
// myOmg: Omg = Omg(All is well)

这里的“编译时保证”是什么?OP已请求接受任意长度的元组,但仅限于特定类型的第1个元素。(不是我的负投票。) - jwvh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接