Shapeless

Shapeless 库主要思想参考了Haskell的HList,以及自身type-class的扩展。它主要用于编译期实现,所以叫做"shapeless",无固定类型、无固定结构化、函数和对象多态等特点。因为它主要是在编译期实现,所以语法上接近于“表述式”。

什么是shapeless

Shapeless is a type class and dependent type based generic programming library for Scala.

因为Shapeless是基于类型,因此对于Scala的类型系统如虎添翼,只要提供少量的样板代码(boilerplate),就可以获得额外的类型安全机制。

因为shapeless的内容比较多,我们从www.scala-exercises.org开始介绍一些主要特性。

Polymorphic functions

多态函数,先看下面的例子:

1
val demo = 42 :: "Hello" :: User("Julien") :: HNil

因为这个HList结构不是固定类型的,如果我们要进行map映射操作,

1
def map[A, B, C](h: Int :: String :: User :: HNil)(f0: Int => A, f1: String => B, f3: User => C)

但是,过多这样的f0,f1,f2参数函数是不切实际的,我们希望只用一个f参数表示,

1
def map[A, B, C](h: Int :: String :: User :: HNil)(f: (Int => A) & (String => B) & (User => C))

可惜Scala里面没有 & 这样的用法。记住一点,只要遇到非结构固定类型的地方,基本可以考虑用shapeless来实现。

1
2
3
4
5
6
7
8
9
10
import shapeless._

object plusOne extends Poly1 {
implicit def caseInt = at[Int](_ + 1)
implicit def caseString = at[String](_ + 1)
implicit def caseUser =
at[User](case User(name) => User(name + 1))
}

demo.map(plusOne)

Generic

Generic,顾名思义,类似于Java的泛型,它主要用于case class/product types 到 HList之间的转换,

1
2
3
4
5
6
7
8
import shapeless.Generic

case class UserWithAge(name: String, age: Int)
val gen = Generic[UserWithAge]
val u = UserWithAge("Julien", 30)

val h = gen.to(u) // returns Julien :: 30 :: HNil
gen.from(h) // return UserWithAge("Julien", 30)

还是那句话,case class也是非结构固定类型的,因此可以扩展,

1
2
3
4
5
6
7
8
9
10
case class Book(author: String, title: String, id: Int, price: Double)
val bookGen = LabelledGeneric[Book]
val tapl = Book("Benjamin Pierce", "Types and Programming Languages", 262162091, 44.11)
val rec = bookGen.to(tapl)
rec('price) should be(44.11)

case class ExtendedBook(author: String, title: String, id: Int, price: Double, inPrint: Boolean)
val bookExtGen = LabelledGeneric[ExtendedBook]
val extendedBook = bookExtGen.from(rec + ('inPrint ->> true))
extendedBook.inPrint should be(true)

Tuples

Shapleless对Tuplex提供了语法,因此可以在tuples上使用HList的方法,

1
2
3
4
5
6
(1, "foo", 12.3).tail
// <console>:14: error: value tail is not a member of (Int, String, Double)
// (1, "foo", 12.3).tail

import shapeless.syntax.std.tuple._
(1, "foo", 12.3).tail // returns (foo,12.3)

Lenses

Lenses,透镜。从名字可以看出,可以通过“透镜”直接操作,

1
2
3
4
5
6
7
8
9
10
11
12
13
// A pair of ordinary case classes ...
case class Address(street: String, city: String, postcode: String)
case class Person(name: String, age: Int, address: Address)

// Some lenses over Person/Address ...
val nameLens = lens[Person] >> 'name
val ageLens = lens[Person] >> 'age
val addressLens = lens[Person] >> 'address
val streetLens = lens[Person] >> 'address >> 'street
val cityLens = lens[Person] >> 'address >> 'city
val postcodeLens = lens[Person] >> 'address >> 'postcode

val person = Person("Joe Grey", 37, Address("Southover Street", "Brighton", "BN2 9UA"))
1
2
3
4
5
6
7
// get & set
ageLens.get(person)
ageLens.set(person)(38)

// nested
streetLens.get(person)
streetLens.set(person)("Montpelier Road")

Abstracting over arity

并不是一个具体的特性,它只是HListGeneric的一个运用。

1
2
3
4
5
import syntax.std.function._
import ops.function._

def applyProduct[P <: Product, F, L <: HList, R](p: P)(f: F)(implicit gen: Generic.Aux[P, L], fp: FnToProduct.Aux[F, L => R]) =
f.toProduct(gen.to(p))
1
2
applyProduct(1, 2)((_: Int) + (_: Int)) should be(3)
applyProduct(1, 2, 3)((_: Int) * (_: Int) * (_: Int)) should be(6)