【问题标题】:Scala type classes for transform on option using higher order functions使用高阶函数进行转换的 Scala 类型类
【发布时间】:2017-01-01 18:50:10
【问题描述】:

我有一些选项,当它们没有时,我不想运行我的转换函数。

当前处理选项的方法如下所示:

def writeOptionalXml[T](content: Option[T], mapFn: T => Xml): Xml =
  content match {
    case Some(c) => mapFn(c)
    case None => NodeSeq.Empty
}

而且效果很好。但我还有其他不是选项但仍然可以为空的输入,例如空字符串、空 xml 节点或某些案例类。

我认为重构为类型类对我来说是一次很好的学习经历。经过大量代码争论后,我发现我需要使用上下文边界来处理选项类型,并认为我正在实现我的类型类梦想。

我卡住的地方是转换函数(在示例中被错误地命名为 mapFn)。 在没有选项的情况下,我想要方法签名: (内容:Option[T],mapFn:T => Xml):Xml 而在其他情况下: (输入:A,mapFn:A => Xml):Xml

我一直在努力更改类型签名,使用 [_] 尝试获得我想要的但无济于事。

我目前拥有的合成版本的代码,一点也不漂亮,看起来像这样:

import scala.annotation.implicitNotFound

object writableTypes extends App {

  type Xml = String
  @implicitNotFound("No member of type class in scope for ${T}")
  trait WritableLike[A] {
    def toXml[B](input: A, mapFn: ((_$1) forSome {type _$1}) => Xml): Xml
  }

  object WritableLike {

    implicit object WritableLikeString extends WritableLike[String] {
      override def toXml[B](input: String, mapFn: ((_$1) forSome {type _$1}) => Xml): Xml =
        mapFn(input)
    }

    implicit def OptionFormat[T: WritableLike]: Object = new WritableLike[Option[T]] {
      override def toXml[B](input: Option[T], mapFn: ((_$1) forSome {type _$1}) => Xml): Xml =
        mapFn(input.get)
    }

    def writeXml[X](input: X, mapFn: ((_$1) forSome {type _$1}) => Xml )(implicit ev: WritableLike[X]): Xml =
      ev.toXml[X](input, mapFn)
  }

  println(WritableLike.writeXml(Option(SomeCaseClass(5)), transformToXml))

  case class SomeCaseClass(content: Int) { def someMethod = ""}

  def transformToXml[T](input: SomeCaseClass): String = input.someMethod

}

不幸的是,这没有编译,因为在这个方法调用中 WritableLike.writeXml(Option(SomeCaseClass(5)), transformToXml)

函数 transformToXml 不满足所需的方法签名。

我已经尝试了很多这种排列,但找不到解决方案,否则很优雅。

我确信有一些简单的方法可以通过将所有内容都设置为选项来解决它,但我更感兴趣的是找到使其真正通用的解决方案。

我不确定我是否已经很好地解释了这一点,这是我第一次尝试编写类型类,我认为这很简单,但我试图解决的特定问题似乎有一些额外的复杂性。

如果有人对使用 Scala 类型系统的泛型编程有更深入的了解,我将不胜感激。

谢谢

【问题讨论】:

    标签: scala optional typeclass higher-order-functions existential-type


    【解决方案1】:

    我认为您对在普通函数中使用的类型类中的mapFn 有点犹豫。您可以构建类型类以仅依赖输入数据。其余的工作是使用类型类的实例完成的。以下是仅依赖输入数据的 WritableLike 类型类定义示例:

    trait WritableLike[A] {
      def toXml(input: A): String
    }
    

    对于本例,我使用String 代替 XML 类型。使用这个定义,我们可以定义一些简单的类型类实例:

    implicit val stringWritableLike: WritableLike[String] = new WritableLike[String] {
      def toXml(input: String): String = s"<text>$input</text>"
    }
    
    implicit val intWritableLike: WritableLike[Int] = new WritableLike[Int] {
      def toXml(input: Int): String = s"<integer>$input</integer>"
    }
    

    在上面的代码中,我为StringInt 类型定义了类型类实例。这两个实例都直接定义了方法 toXml 的输入如何转换为 XML 字符串,而不依赖于另一个函数来提供转换。这意味着当我们为任何一种类型应用类型类时,无论值如何,我们都将始终使用相同的转换。

    如果我们想重载某些值的转换,我们想用其他类型包装这些值,并为该类型提供一个类型类实例。

    case class Name(name: String) extends AnyVal
    
    implicit val nameWritableLike: WritableLike[Name] = new WritableLike[Name] {
      def toXml(input: Name): String = s"<name>${input.name}</name>"
    }
    

    这里我们定义了一个用于表示名称的案例类和一个用于将名称转换为 XML 的特定类型类。

    我将这些实例定义为值,但我不妨使用defobject。重要的部分是,无论使用这些关键字中的哪一个,实例都是隐式的。隐含性允许我们稍后获取这些实例,而无需显式引用它们。

    在某些情况下,您必须使用def 而不是val 来定义类型类实例:当您的类型类实例依赖于另一个类型类实例时需要它。我们可以以Option 为例:

    implicit def optionWritableLike[A](implicit instanceForA: WritableLike[A]): WritableLike[Option[A]] =
      new WritableLike[Option[A]] {
        def toXml(input: Option[A]): String = input match {
          case Some(a) => instanceForA.toXml(a)
          case None => "<empty />"
        }
      }
    

    在上面的代码中,我们为Option 类型定义了一个类型类实例,它适用于Option 类型参数也具有类型类实例的所有值。这里我们将内部类型表示为函数类型参数A。由于我们不知道A 是什么,我们需要以某种方式获取该类型的类型类实例。我们可以通过为我们需要的确切类型类实例添加一个隐式参数来实现这一点。最后,使用隐式实例,我们可以将底层值转换为 XML。

    类型类实例的隐式参数在实例和类型参数的实例之间创建依赖关系。如果类型参数的实例存在,我们可以使用依赖它的实例。如果没有,我们不能使用该实例。

    我们可以使用另一种语法来声明类型类依赖关系:

    implicit def optionWritableLike[A: WritableLike]: WritableLike[Option[A]] =
      new WritableLike[Option[A]] {
        def toXml(input: Option[A]): String = input match {
          case Some(a) => implicitly[WritableLike[A]].toXml(a)
          case None => "<empty />"
        }
      }
    

    这里我们将类型类依赖声明为类型参数定义的一部分。声明A: WritableLike 意味着类型参数必须具有类型类WritableLike 的类型类实例才能工作。我们可以使用 Scala 的关键字implicitly 访问该实例。 implicitly 调用有点冗长,所以习惯上在类型类的伴生对象中定义一个辅助函数:

    object WritableLike {
      def apply[A: WritableLike]: WritableLike[A] = implicitly[WritableLike[A]]
    }
    

    现在我们可以像这样访问类型类实例:WritableLike[A].toXml(a)

    在实例定义之外使用类型类时,辅助方法也可以提供帮助:

    WritableLike[String].toXml("foobar")               // <text>foobar</text>
    WritableLike[Int].toXml(123)                       // <integer>123</integer>
    WritableLike[Name].toXml(Name("Alan"))             // <name>Alan</name>
    WritableLike[Option[String]].toXml(Some("foobar")) // <text>foobar</text>
    WritableLike[Option[Int]].toXml(None)              // <empty />
    

    【讨论】:

    • 我希望这个答案有帮助。如果您有任何问题,请在此处发表评论,我会看看是否可以扩展我的答案。 :)
    • 您好 Jaako,感谢您的回答。在更广泛的上下文中,有几个格式化程序,每个格式化程序都从案例类、字符串、日期、其他节点序列等(如 NodeSeq)的组合中生成 xml NodeSeq。偶尔会有空字符串,nodeseqs 或 None 的 Option[caseclass]。有时在多个地方(每个格式化程序)使用案例类来生成 xml 文档的不同部分,具有不同的 xml 标签,因此使用传递函数生成节点的灵活性至关重要。
    • 商业案例很好,但是我想探索 Scala 中类型类模式的强大功能和局限性。也许这是语言或模式的限制?感觉应该是可能的。
    • 您可以尝试使用我的回答中描述的类型(Name 类型)对这些上下文进行编码。当类型用于分派函数调用时,类型类往往很有用。如果您仍然觉得类型类太麻烦,那么普通函数应该就可以了。 :-)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-02
    • 2019-06-30
    • 1970-01-01
    • 1970-01-01
    • 2012-10-25
    • 1970-01-01
    相关资源
    最近更新 更多