使用高阶函数进行变换的Scala类型类

时间:2016-08-25 11:04:27

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

我有一些选择,当它们不存在时,我想要不想运行我的转换功能。

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

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

它很棒。但我也有其他输入不是选项,但仍然可以为空,例如空字符串,空xml节点或某些案例类。

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

我遇到的是转换函数(在示例中命名为mapFn)。 在none选项的情况下,我想要方法签名: (内容:选项[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类型系统通用编程的人。

由于

1 个答案:

答案 0 :(得分:0)

我认为你在普通函数中使用的类型类中有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>"
}

这里我们定义一个用于表示名称的case类和一个用于将名称转换为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。

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

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

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意味着type参数必须具有类型类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 />