Scala类型类的小应用之CSV Encoder
by 壮衣
在上一篇博客《Scala由类的动态扩展想到类型类》中提到了类型类,写了一个类型类:Hello[A]并实现了一些类型类的实例。但是为了更容易理解,列举的例子比较简单。这一篇博客我们将对类型类的知识做深入一点的应用:想一下我们需要写一个库用于将任意一种数据类型转换成String类型用于保存为csv文件。在进行数据类型转换时有如下代码:
scala> List(List(1, 2, 3), List(4, 5, 6)).asCsv
res2: String =
1,2,3
4,5,6
scala> List(("zdx", 28), ("ygy", 29)).asCsv
res4: String =
zdx,28
ygy,29
scala> case class Person(name: String, age: Int, height: Double)
defined class Person
scala> List(Person("zdx", 28, 145.0), Person("ygy", 29, 185.0)).asCsv
res0: String =
zdx,28,145.0
ygy,29,185.0
我们先定义类型类:
trait Encoder[A] {
def apply(a: A): String
}
然后添加一些基本的类型类实例:
object Encoder {
def apply[A: Encoder](a: A): String = implicitly[Encoder[A]].apply(a)
implicit val shortEncoder: Encoder[Short] = new Encoder[Short] {
override def apply(a: Short): String = a.toString
}
implicit val intEncoder: Encoder[Int] = new Encoder[Int] {
override def apply(a: Int): String = a.toString
}
implicit val LongEncoder: Encoder[Long] = new Encoder[Long] {
override def apply(a: Long): String = a.toString
}
implicit val floatEncoder: Encoder[Float] = new Encoder[Float] {
override def apply(a: Float): String = a.toString
}
implicit val doubleEncoder: Encoder[Double] = new Encoder[Double] {
override def apply(a: Double): String = a.toString
}
implicit val charEncoder: Encoder[Char] = new Encoder[Char] {
override def apply(a: Char): String = a.toString
}
implicit val stringEncoder: Encoder[String] = new Encoder[String] {
override def apply(a: String): String = a
}
implicit val booleanEncoder: Encoder[Boolean] = new Encoder[Boolean {
override def apply(a: Boolean): String = a.toString
}
}
可以先测试下这些类型类实例:
scala> import org.forcestudy.csvz.Encoder
import org.forcestudy.csvz.Encoder
scala> import Encoder._
import Encoder._
scala> Encoder[Int](1)
res3: String = 1
scala> Encoder[String]("zdx")
res5: String = zdx
scala> Encoder[Boolean](true)
res8: String = true
为类型类注入方法:
trait EncoderSyntax[A] {
def asCsv: String
}
object EncoderSyntax {
implicit def encoderSyntax[A: Encoder](a: A): EncoderSyntax[A] = new EncoderSyntax[A] {
override def asCsv: String = implicitly[Encoder[A]].apply(a)
}
}
现在可以再测试下这些类型类的实例:
scala> import org.forcestudy.csvz.EncoderSyntax._
import org.forcestudy.csvz.EncoderSyntax._
scala> 1.asCsv
res9: String = 1
scala> "zdx".asCsv
res10: String = zdx
scala> true.asCsv
res11: String = true
有了这个基本类型的Encoder[A]的实例之后我们可以添加Encoder[List[A]]的实例了:
implicit def listValEncoder[A <: AnyVal](implicit encoder: Encoder[A]): Encoder[List[A]] = new Encoder[List[A]] {
override def apply(a: List[A]): String = a.map(encoder.apply).mkString(",")
}
来测试下Encoder[List[A]]:
scala> Encoder[List[Int]](List(1, 2, 3))
res1: String = 1,2,3
scala> Encoder[List[List[Int]]](List(List(1, 2, 3), List(4, 5, 6)))
<console>:16: error: could not find implicit value for evidence parameter of type org.forcestudy.csvz.Encoder[List[List[Int]]]
Encoder[List[List[Int]]](List(List(1, 2, 3), List(4, 5, 6)))
在测试Encoder[List[List[Int]]]的时候编译报错, 提示找不到Encoder[List[List[Int]]]的实例,我们有提供Encoder[List[A]] 的实例,但是这个A必须是AnyVal的子类而List[Int]是AnyRef的子类。针对这种情况我还得增加一个实例:
implicit def listRefEncoder[A <: AnyRef](implicit encoder: Encoder[A]): Encoder[List[A]] = new Encoder[List[A]] {
override def apply(a: List[A]): String = a.map(encoder.apply).mkString("\n")
}
好了现在可以测试Encoder[List[List[A]]]了:
scala> import org.forcestudy.csvz.EncoderSyntax._
import org.forcestudy.csvz.EncoderSyntax._
scala> List(List(1, 2, 3), List(4, 5, 6)).asCsv
res0: String =
1,2,3
4,5,6
scala> List(List('z', 'd', 'x'), List('y', 'g', 'y')).asCsv
res1: String =
z,d,x
y,g,y
再来看下转换元组类型:
scala> Encoder[(String, Int)](("zdx", 29))
<console>:19: error: could not find implicit value for evidence parameter of type org.forcestudy.csvz.Encoder[(String, Int)]
Encoder[(String, Int)](("zdx", 29))
控制台提示我们并没有实现Encoder[(String, Int)]实例,当然我们可以实现Encoder[(String, Int)]实例,那我遇到更多的元组类型呢? 像Encoder[(String, Int, Double)];或者我们该如何处理case class?是不是需要为每种case class都实现类型类实例?假如那样做的话那真是一件繁重的事情,幸好有shapeless,我们将使用shapeless中的Automatic type class instance derivation来实现我们需求,先看下代码:
libraryDependencies ++= Seq(
"com.chuusai" %% "shapeless" % "2.2.5"
)
object Encoder extends ProductTypeClassCompanion[Encoder] {
// code
object typeClass extends ProductTypeClass[Encoder] {
override def project[F, G](instance: => Encoder[G], to: (F) => G, from: (G) => F): Encoder[F] = new Encoder[F] {
override def apply(a: F): String = instance.apply(to(a))
}
override def emptyProduct: Encoder[HNil] = new Encoder[HNil] {
override def apply(a: HNil): String = ""
}
override def product[H, T <: HList](ch: Encoder[H], ct: Encoder[T]): Encoder[::[H, T]] = new Encoder[::[H, T]] {
override def apply(a: ::[H, T]): String =
if (ct.apply(a.tail).isEmpty) ch.apply(a.head)
else List(ch.apply(a.head), ct.apply(a.tail)).mkString(",")
}
}
}
先不深究这一块代码, 我们先来测试下元组的Encoder实例,以及case class的Encoder实例:
scala> import org.forcestudy.csvz.EncoderSyntax._
import org.forcestudy.csvz.EncoderSyntax._
scala> import org.forcestudy.csvz.Encoder
import org.forcestudy.csvz.Encoder
scala> import org.forcestudy.csvz.Encoder._
import org.forcestudy.csvz.Encoder._
scala> Encoder[(String, Int)](("zdx", 29))
res0: String = zdx,29
scala> case class Person(name: String, age: Int, height: Double)
defined class Person
scala> Encoder[Person](Person("zdx", 29, 145.0))
res1: String = zdx,29,145.0
如上有了shapeless我们并不需要为具体case class和元组添加类型类实例。回到文章的开头我们再来测试下文章开头的几个例子:
image.png
到此代码实现已经可以满足我们的需求了, 但是对于shapeless是如何实现各种case class和元组类型类的实例我们只是列出了代码,并没有做说明,这是另一个问题了,我们留做后面博文说明。