类型ValueOf函数可以在方法的上下文参数列表中使用来选择单例类型的单个居民,或者如果类型参数不是单例则拒绝隐式解析。

enum Color { case red, green, blue }
def singleInhabitantOf[T](using  holder: ValueOf[T]): T = holder.value
println(singleInhabitantOf[Color.red.type] == Color.red) // outputs true

但它是有限的。它不适用于组件类型均为单例的元组。

singleInhabitantOf[(Color.red.type, Color.blue.type)] // compile error

错误消息是:No singleton value available for (Color.red, Color.blue);适合ValueOf综合的单例类型包括文字和稳定路径。

所以我尝试以这种方式为元组创建一个版本:

import scala.Tuple

type ValuesOf[T <: Tuple] <: Tuple = T match {
    case EmptyTuple => EmptyTuple
    case h *: t => ValueOf[h] *: ValuesOf[t]
}

def singleInhabitantsOf[T<:Tuple](using holder: ValuesOf[T]): Tuple = holder // a tuple mapping is missing here but how to implement it is another question.

singleInhabitantsOf[(Color.red.type, Color.blue.type)] // Copile error

不幸的是,编译器抱怨说:没有为参数持有者找到类型 ValuesOf[(Color.red, Color.blue)] 的给定实例。显然,它在隐式上下文中查找元组实例,而不是用单例实例合成元组。

然而

implicitly[summonAll[ValuesOf[T]]]

编译并运行良好。所以我可以summonAll像这样重写该方法

inline def singleInhabitantsOf2[T <: Tuple]: Tuple = summonAll[ValuesOf[T]] 

但这个解决方案对我的最终目的没有用,为此,单例性检查应该在方法签名中,这样如果出现问题,编译器就不会开始处理主体。

关于如何定义与上下文参数列表中的元组一起使用的 ValueOf 有什么想法吗?

7

  • 2
    为什么不只使用自定义类型类的简单头尾派生,如下所示:


    – 

  • @LuisMiguelMejíaSuárez 该方法GetSingletonValue.of适用于文字参数。但由于某种原因,当应用于Mirror.Sum.MirrorElemTypes.以下 def 不起作用:scala inline def summandsOf[T](using mirror: Mirror.SumOf[T]): Tuple = GetSingletonValue.of[T] 要使其工作,必须使用 SummonInlinescala inline def summandsOf[T](using mirror: Mirror.SumOf[T]): Tuple = summonInline[GetSingletonValue[mirror.MirroredElemTypes]].value 不知道为什么。


    – 


  • 将所有决策者移至签名也可以inline def summandsOf[T](using mirror: Mirror.SumOf[T])(using sv: GetTupleSingletonValue[mirror.MirroredElemTypes]): Tuple = sv.value


    – 


  • 读者朋友们,这又如何呢? – 如果这解决了您的问题,我很高兴将其作为答案发布。 CC @DmytroMitin 非常感谢您提供的任何反馈:)


    – 


  • 1
    那么第二个解决方案与原始解决方案相同,只是添加了ofEnum.ofEnum这只是一种方便的方法,因为valueOf假设您进行间接Mirror获取enum.无论如何,现在发布代码作为答案。


    – 


2 个回答
2

类型类和匹配类型是在 Scala 3 中执行类型级计算的两种方法(如 Scala 2 中的类型投影和类型类、Haskell 中的类型族和类型类)。

混合类型类和匹配类型可能很棘手:


看来您想要类型类而不是匹配类型

trait ValuesOf[T <: Tuple]:
  def value: T

object ValuesOf:
  given ValuesOf[EmptyTuple] with
    val value = EmptyTuple

  given [h, t <: Tuple](using vh: ValueOf[h], vt: ValuesOf[t]): ValuesOf[h *: t] with
    val value = vh.value *: vt.value

def singleInhabitantsOf[T <: Tuple](using holder: ValuesOf[T]): T = holder.value

singleInhabitantsOf[(Color.red.type, Color.blue.type)] // (red,blue)

使用匹配类型,您可以执行以下操作

type SingleInhabitantsOf[T <: Tuple] <: Tuple = T match
  case EmptyTuple => EmptyTuple
  case h *: t => h *: SingleInhabitantsOf[t]

inline def singleInhabitantsOf0[T <: Tuple]: SingleInhabitantsOf[T] =
  inline erasedValue[T] match
    case _: EmptyTuple => EmptyTuple
    case _: (h *: t) => valueOf[h] *: singleInhabitantsOf0[t]

// to specify return type, not necessary
inline def singleInhabitantsOf[T <: Tuple]: T = summonFrom {
  case given (SingleInhabitantsOf[T] =:= T) => singleInhabitantsOf0[T]
}

singleInhabitantsOf[(Color.red.type, Color.blue.type)] // (red,blue)

对元组使用标准操作,您可以执行以下操作

type ValuesOf[T <: Tuple] = Tuple.Map[T, ValueOf]

type InvValueOf[V] = V match
  case ValueOf[a] => a

type SingleInhabitantsOf[T <: Tuple] = Tuple.Map[ValuesOf[T], InvValueOf]

// can't express return type
inline def singleInhabitantsOf0[T <: Tuple] /*: Tuple.Map[? <: ValuesOf[T], InvValueOf]*//*: SingleInhabitantsOf[T]*/ =
  val valuesOf = summonAll[ValuesOf[T]]
  valuesOf.map[InvValueOf]([b] => (y: b) => y match
    case v: ValueOf[a] => v.value
  ): Tuple.Map[valuesOf.type, InvValueOf]

// to specify return type, not necessary
inline def singleInhabitantsOf[T <: Tuple]: T = summonFrom {
  case _: (SingleInhabitantsOf[T] =:= T) => singleInhabitantsOf0[T]
}

singleInhabitantsOf[(Color.red.type, Color.blue.type)] // (red,blue)

但这个解决方案对我的最终目的没有用,为此,单例性检查应该在方法签名中,这样如果出现问题,编译器就不会开始处理主体。

听起来有点奇怪的要求。您始终可以从左侧“隐藏”右侧的隐式参数。在Scala 2中你可以重写

def foo[A](implicit tc: TC[A]): Unit = ()

作为

def foo[A]: Unit = macro fooImpl[A]

def fooImpl[A: c.WeakTypeTag](c: blackbox.Context): c.Tree = {
  import c.universe._
  c.inferImplicitValue(weakTypeOf[TC[A]])
  q"()"
}

在Scala 3中你可以重写

def foo[A](using TC[A]): Unit = ()

作为

inline def foo[A]: Unit =
  summonInline[TC[A]]
  ()

也许您可以更多地了解您的实际问题(也许开始一个新问题),我们可以看看它是否可以通过匹配类型、summonAllconstValueTuple来自 的其他内容来解决scala.compiletime.*


但我认为将编译错误作为决策者并不是一个好的做法。这是不友好且难以理解的错误消息。

我并不是要争论代码风格,但错误消息取决于开发人员

def foo[A](using TC[A]): Unit = ()

foo[Int] // No given instance of type App.TC[Int] was found for parameter x$1 of method foo in object App
inline def foo[A]: Unit =
  summonInline[TC[A]]
  ()

foo[Int] // No given instance of type App.TC[Int] was found
def foo[A](using @implicitNotFound("No TC!!!") tc: TC[A]): Unit = ()

foo[Int] // No TC!!!
inline def foo[A]: Unit =
  summonFrom {
    case _: TC[A] => ()
    case _ => error("No TC!!!")
  }

foo[Int] // No TC!!!

我并不坚持认为您应该始终使用隐式“在右侧”而不是隐式“在左侧”。我只是想让你知道你有选择。当然,“在左边”的隐式更为标准。但“在右边”的隐式有时可能更灵活。例如,请参阅我在(已经提到的)。我在方法体中使用了深深嵌套在“右侧”的隐式。对于“左边”的隐式来说,这可能更难实现。

9

  • 关于“你总是可以从左到右隐藏隐式参数”,如果同一类型有两个给定怎么办?它们应该是互斥的。这种排他性是否应该完全由签名决定,还是也可以由主体的编译性决定?


    – 


  • summonInline当该方法触发时,您可以自定义隐式未找到错误消息(使用@implicitNotFound)吗?


    – 

  • @Readren是的,你可能会遇到自定义编译错误。c.echo// c.info// c.warning/在 Scala 2 中,在 Scala 3 中c.error不确定我是否理解“排他性”。也许一些小代码片段用于说明?例如,在 Scastie,如果不是在新问题中。c.abortscala.compiletime.error


    – 


  • @Readren summonFrom { case given TC[A] => ???; case _ => error("no TC") }


    – 

  • @Readren如果您有两个隐式(给定),则可以在调用时解析隐式(基于不太特殊的类型/更特殊的类型,较低优先级的隐式/较高优先级的隐式),或者它是一个错误(不明确的隐式)。这对于左侧的隐式/使用参数或右侧的c.inferImplicitValue/是相同的。summonInline


    – 


另一种方法是仅使用类型类的传统头尾派生。

我几乎确定您可以重用现有的
ValueOf,但我个人更喜欢编写自己的一个以避免出现任何问题:

trait GetSingletonValue[A]:
  def value: A
  
object GetSingletonValue:
  /** Construct a GetSingletonValue for any value for which there is a ValueOf instance. */  
  given [A](using ev: ValueOf[A]): GetSingletonValue[A] with
    override final val value: A = ev.value

  /** Base case for the Tuple recursion. */
  given GetSingletonValue[EmptyTuple] with
    override final val value: EmptyTuple = EmptyTuple

  /** Recursive case for the Tuple recursion. */
  given [A, T <: Tuple](using head: GetSingletonValue[A], tail: GetSingletonValue[T]): GetSingletonValue[A *: T] with
    override final val value: A *: T = head.value *: tail.value
end GetSingletonValue

然后,您可以为任何单例请求一个实例:

summon[GetSingletonValue[Color.Green.type]].value // Green

以及任何Tuple单身人士:

summon[GetSingletonValue[(Color.Red.type, Color.Blue.type)]].value // (Red, Blue)

然后,您可以添加一些有用的辅助方法来减少样板代码:

inline def apply[A](using ev: GetSingletonValue[A]): ev.type = ev

inline def ofValue[A](using ev: GetSingletonValue[A]): A = ev.value

并像这样使用它们:

GetSingletonValue.ofValue[Color.Green.type] // Green
GetSingletonValue.ofValue[(Color.Red.type, Color.Blue.type) // (Red, Blue)

现在,您希望能够获取给定 的所有单例enum

为此,我们需要具体化一个表示所有枚举类型的元组类型。值得庆幸的是,
Scala 3已经以Mirror.SumOf[T]#MirroredElemTypes.

val colorMirror = summon[scala.deriving.Mirror.SumOf[Color]]
GetSingletonValue.ofValue[colorMirror.MirroredElemTypes] // (Red, Green, Blue)

Mirror然而,为了请求这些值而必须具体化我们自己是多余的样板。

因此,我们可以提供一个很好的帮手:

inline def ofEnum[E](using m: scala.deriving.Mirror.SumOf[E], ev: GetSingletonValue[m.MirroredElemTypes]): m.MirroredElemTypes = ev.value

如果您愿意,也可以这样编码:

inline def ofEnum2[E](using m: scala.deriving.Mirror.SumOf[E]): m.MirroredElemTypes =
    scala.compiletime.summonInline[GetSingletonValue[m.MirroredElemTypes]].value

它们本质上是同一件事。就我个人而言,我更喜欢第一个,它对于Scala 2用户来说更常见,并且需要更少的宏魔法。

两者都按预期工作:

GetSingletonValue.ofEnum[Color] // (Red, Green, Blue)
GetSingletonValue.ofEnum2[Color] // (Red, Green, Blue)

您可以看到代码在中运行。


最后一点,据我所知,在Scala 3中,由于推导的“高级”构造,不再推荐头尾方法。但是,就我个人而言,由于很多原因,我总是很难使用它们。Shapeless 3是实现高级思想的更好方法,但我仍然更喜欢简单的递归。

3

  • 不错的方法,路易斯


    – 

  • 1
    @LuisMiguelMekíaSuarez Dmytro 的解决方案不是也使用头尾推导吗?除此之外还有其他方法可以解决这个问题吗?


    – 


  • 1
    @Readren哦,对了,最后我有点被宏观的东西分散了注意力。但是,是的,大部分都是一样的,我的更一般一些。


    –