在 Scala 中使用带有 PureConfig 的泛型类型

Posted

技术标签:

【中文标题】在 Scala 中使用带有 PureConfig 的泛型类型【英文标题】:Using generic type with PureConfig in Scala 【发布时间】:2022-01-22 04:02:00 【问题描述】:

我正在尝试从具有泛型类型的方法调用 PureConfig 的 loadOrThrow:

def load[T: ClassTag](path: String): T = 
    import pureconfig.generic.auto._
    ConfigSource.file(path).loadOrThrow[T]

当我尝试从主类调用它时,出现以下错误:

could not find implicit value for parameter reader: pureconfig.ConfigReader[T]
    ConfigSource.file(path).loadOrThrow[T]

我可以在主类中没有import pureconfig.generic.auto._ 的情况下解决这个问题吗?

【问题讨论】:

def load[T : ClassTag : ConfigReader](path: String): T = 如错误所示。 @LuisMiguelMejíaSuárez import pureconfig.generic.auto._ 在主类中仍然是必需的 不可能派生“通用”编解码器。通用 = 无类型,这就是原始的 Config 类。如果要解析Config,则必须在硬编码其类型的任何地方定义/生成编解码器。如果您没有在任何地方选择类型......那么您根本无法使用编解码器和 PureConfig。在您的情况下,答案是:在 main 中导入 pureconfig.generic.auto 或仅使用原始 TypeSafe 配置。 如果您只想避免在 main 中导入,还可以选择生成编解码器并将其放入 T 的伴随对象中。但是您仍然必须选择类型 T 作为特定的东西,并在伴生中派生编解码器。 @Niccko 所以,为了读取配置文件并转换为配置类 Tpurecofing 需要 ConfigReader[T],正如 Mateusz 解释的那样,在你调用 @ 的地方987654330@ 并且您将泛型类型参数 T 固定为具体类型,您需要提供隐含的证据证明 Configreader 存在于这种具体类型 T 中,有多种方法可以实现这一点。一种是离开调用站点使用import pureconfig.generic.auto._ 进行推导,另一种是在配置类的伴随对象中进行推导。 【参考方案1】:

总结 cmets 并解释这个编解码器是如何工作的。

当你这样做时:

def something[T: ConfigReader] = ...

你正在使用语法糖

// Scala 2
def something[T](implicit configReader: ConfigReader[T]) = ...

// Scala 3
def something[T](using configReader: ConfigReader[T]) = ...

在你写的时候调用网站上:

something[T]

编译器确实可以

something(configReaderForT /* : ConfigReader[T] */)

所以基本上它是编译器支持的基于类型的依赖注入。并且依赖注入必须从某个地方获取要传递的值。

编译器如何获取那个来传递它?它必须通过它在范围内的类型来找到它。应该有一个此类型的明确最接近的值(或 def 返回此值)标记为 implicit (Scala 2) 或 given (Scala 3)。

// Scala 2
implicit val fooConfigReader: ConfigReader[Foo] = ...
something[Foo] // will use fooConfigReader

// Scala 3
given fooConfigReader: ConfigReader[Foo] = ...
something[Foo] // will use fooConfigReader

Scala 3 基本上更容易区分哪个是价值的定义 - given - 哪个是依赖于从外部某处提供价值的地方 - using。 Scala 2 有一个词来形容它——implicit——这引起了很多混乱。

您必须自己定义此值/方法或将其导入 - 在需要它的范围内 - 否则编译器将仅尝试查看对您的类型有贡献的所有类型的伴随对象 T - 如果 T 是具体的。 (或者如果在编译器错误消息中找不到它,则失败)。

// Example of companion object approach
// This is type Foo
case class Foo()
// This is Foo's companion object
object Foo 
  // This approach (calling derivation manually) is called semiauto
  // and it usually needs a separate import
  import pureconfig.generic.semiauto._

  implicit val configReader: ConfigReader[Foo] = deriveReader[Foo]


// By requiring ConfigReader[Foo] (if it wasn't defined/imported
// into the scope that needs it) compiler would look into:
// * ConfigReader companion object
// * Foo companion object
// ConfigReader doesn't have such instance but Foo does.

如果T 是通用的,那么您必须将implicit/given 作为参数传递 - 但是您只是推迟了必须指定它的时刻并让编译器找到/生成它。

// Tells compiler to use value passed as parameter
// as it wouldn't be able to generate it based on generic information

// implicit/using expressed as "type bounds" (valid in Scala 2 and 3)
def something[T: ConfigReader] = ...
// Scala 2
def something[T](implicit configReader: ConfigReader[T]) = ...
// Scala 3
def something[T](using configReader: ConfigReader[T]) = ...

// It works the same for class constructors.

在 PureConfig 的情况下,pureconfig.generic.auto 包含 implicit defs,它为指定的 T 生成值。如果要生成它,则必须将其导入到需要该特定实例的位置。您可以在伴随对象中执行此操作,以使其在此特定类型需要此 ConfigReader 的任何地方自动导入,或将其导入 main (或任何其他指定 T 的地方)。无论哪种方式,您都必须在某处派生它,然后将此 [T: ConfigReader](implicit configReader: ConfigReader[T]) 添加到不应将 T 硬编码到任何东西的所有方法的签名中。

总结您的选择是:

将导入保留在 main 中(如果您将 T 硬编码为 main 中的特定类型) 派生它并在其他地方定义为implicit,然后从那里导入(有些人在traits 中这样做,然后将它们混合,但我不喜欢这种方法) 派生它并在伴随对象中定义为implicit

只要您希望自己的配置是解析值而不是非类型化 JSON (HOCON) 而无需自己编写这些编解码器,您就必须在某处执行自动(或半自动)派生。

【讨论】:

以上是关于在 Scala 中使用带有 PureConfig 的泛型类型的主要内容,如果未能解决你的问题,请参考以下文章

在 Scala 中使用带有 java.nio.channels.ClosedChannelException 的 com.azure.storage.blob 包的基本 blob 下载失败

带有 VScode 的 Scala

使用 Scala 类作为带有 pyspark 的 UDF

使用 Apache Zeppelin 重新运行带有 -deprecation 的 Scala 代码

未知工件 sbtplugin 带有 scala 2.12 的超级安全编译器

带有 Play2.4 和 scala 的 Google Guice 的循环依赖错误