在这篇简短的文章中,我想解释一下如何在Scala中使用typeclass模式来实现即席多态性。 在本文中,我们不会过多讨论理论上的细节,我要向您展示的主要是,创建一个灵活的可扩展模型确实很容易,而不必将您的域模型与特定的实现或特性绑定在一起。 Internet上有很多有关Scala类型类的资源,但是我找不到很好的参考,所以这就是为什么我创建了这个。 在此示例中,我们将看一下Haskell的read类型类的一种非常务实的(并且是幼稚的)实现。 该类型类允许您以通用方式将字符串转换为特定类型。
我们要使用此类型类完成的操作如下(完整的示例可以在这里找到: https : //gist.github.com/josdirksen/9051baf09003dac37386 )
println(Readable[Double].read("10"))
println(Readable[Int].read("10"))
println(Readable[String].read("Well duh!"))
println(Readable[List[Char]].read("Well duh!"))
println(Readable[List[String]].read("Using:A:Separator:to:split:a:String"))
// we can also use the read function directly
println("20".read[Double]);
println("Using:A:Separator:to:split:a:String".read[List[Char]]);
println("Using:A:Separator:to:split:a:String".read[List[String]]);
println(Readable[Task].read("10|Title Text|Title Content"))
println("20|Another title Text|Another title Content".read[Task])
使用Readable类型类,我们提供了一种将String转换为特定类型的通用方法。 在上面的示例中,我们使用类型类将String转换为一些基本类型,还转换为不同的列表和特定的案例类。 基本类型的功能并不是真的有用,因为scala String对象已经提供了toDouble,toString等函数。 但是,通过这种方式,您不需要知道要调用的特定函数,而只需指定所需的目标类型即可:)但是,与在此处看到的Task案例类相比,它变得更加有趣。 正如在其余代码中所看到的,通过使用隐式,我们可以简单地添加对此case类的支持,而不必更改String类或Task类的实现。
要实现类型类,我们首先必须定义特征,该特征定义了我们需要实现的功能。 在此示例中,我们只有一个功能:
/**
* The readable trait defines how objects can be converted from a string
* representation to the objects instance. For most of the standard types
* we can simply use the toType function of String.
*/
trait Readable[T] {
def read(x: String): T
}
read函数应该将String转换为指定的类型T。现在,我们已经定义了特征,让我们看一下伴随对象,该对象包含一些帮助器类和简单的实现:
/**
* Companion object containing helper functions and standard implementations
*/
object Readable {
/**
* Helper function which allows creation of Readable instances
*/
def toReadable[T](p: String => T): Readable[T] = new Readable[T] {
def read(x: String): T = p(x)
}
/**
* Allow for construction of standalone readables, if the ops aren't used
*/
def apply[A](implicit instance: Readable[A]): Readable[A] = instance
// Using the toReadable creates cleaner code, we could also explicitly
// define the implicit instances:
//
// implicit object ReadableDouble extends Readable[Double] {
// def read(s: String): Double = s.toDouble
// }
// implicit object ReadableInt extends Readable[Int] {
// def read(s: String): Int = s.toInt
// }
implicit val ReadableDouble = toReadable[Double](_.toDouble)
implicit val ReadableInt = toReadable[Int](_.toInt)
implicit val ReadableLong = toReadable[Long](_.toLong)
implicit val ReadableString = toReadable[String](new String(_))
implicit val ReadableBoolean = toReadable[Boolean](_.toBoolean)
implicit val ReadableCharList = toReadable[List[Char]](_.toCharArray.toList)
implicit val ReadableStringList = toReadable[List[String]](_.split(':').toList)
}
代码应该不难理解。 在这里,我们要做的是创建许多Readable实现。 重要的是要注意,我们使用了隐式参数,以便以后可以将它们拉入范围。 至此,我们已经可以开始使用typeclass了。 为此,我们导入隐式对象并使用Readable类型类,如下所示:
import Readable._
import Readable.ops._
// now we can just get an instance of a readable and call the read function
// to parse a string to a specific type.
println(Readable[Double].read("10"))
println(Readable[Int].read("10"))
println(Readable[String].read("Well duh!"))
println(Readable[List[Char]].read("Well duh!"))
println(Readable[List[String]].read("Using:A:Separator:to:split:a:String"))
容易吧? Scala将查找与指定类型匹配的隐式并将String转换为该类型。 很好,但是看起来并不那么好。 我们需要实例化一个Readable(即使不是太多的代码)并调用read来转换String。 我们可以通过使用为我们进行转换的read函数扩展String类来使其变得更加容易。 为此,将以下内容添加到Readable随播对象。
/**
* Extend the string object with a read function.
*/
object ops {
implicit class pp[T](s: String) {
/**
* The type parameter should have an implcit Readable in scope. Use
* implicitly to access it and call the read function
*/
def read[T: Readable]= implicitly[Readable[T]].read(s)
}
}
这些操作定义了一个隐式转换,该转换将读取函数添加到String对象。 剩下的就是导入ops隐式函数,我们可以直接使用read:
import Readable.ops._
// we can also use the read function directly
println("20".read[Double]);
println("Using:A:Separator:to:split:a:String".read[List[Char]]);
println("Using:A:Separator:to:split:a:String".read[List[String]]);
同样的方法也可以用于为案例类创建读取函数:
// creating custom read function can be done without tying a case class
// to the reads implementation. In the following sample assume we
// serialize it to a string with | as separators:
// 10|Title Text|Title Content
case class Task(id: Long, title: String, content: String)
// simple convert the incoming string to a Task
implicit val readableTask = toReadable(
_.split('|') match {
case Array(id: String, title: String, content: String) => new Task(id.read[Long], title, content)
}
)
println(Readable[Task].read("10|Title Text|Title Content"))
println("20|Another title Text|Another title Content".read[Task])
就是这样。 通过仅实现Readable [Task]特性,可以以与其他对象相同的方式处理自定义Task案例类。