您的位置:首页 > 其它

函数式思维: 函数设计模式,第 3 部分

2015-11-30 16:10 357 查看
解释器模式和扩展语言

Gang of Four 的解释器设计模式 (Interpreter design pattern) 鼓励在一个语言的基础上构建一个新的语言来实现扩展。大多数函数式语言都能够让您以多种方式(如操作符重载和模式匹配)对语言进行扩展。尽管 Java™ 不支持这些技术,下一代 JVM 语言均支持这些技术,但其具体实现细则有所不同。在本文中,Neal Ford 将探讨 Groovy、Scala 和 Clojure 如何通过以 Java 无法支持的方式来实现函数式扩展,从而实现解释器设计模式的目的。

在本期
函数式思维 的文章中,我将继续研究 Gang of Four (GoF) 设计模式(参阅
参考资料)的函数式替代解决方案。在本文中,我将研究最少人了解,但却是最强大的模式之一:解释器 (Interpreter)。

解释器的定义是:

给定一个语言,定义其语法表示,以及一个使用该表示来解释语言中的句子的解释器。
换句话说,如果您正在使用的语言不适用于解决问题,那么用它来构建一个适用的语言。关于该方法的一个很好的示例出现在 Web 框架中,如 Grails 和 Ruby on Rails(参阅

参考资料),它们扩展了自己的基础语言(分别是 Groovy 和 Ruby),使编写 Web 应用程序变得更容易。

这种模式最少人了解,因为构建一种新的语言并不常见,需要专业的技能和惯用语法。它是最强大的 设计模式,因为它鼓励您针对正在解决的问题扩展自己的编程语言。这在 Lisp(因此 Clojure 也同样)世界是一个普遍的特质,但在主流语言中不太常见。

当使用禁止对语言本身进行扩展的语言(如 Java)时,开发人员往往将自己的思维塑造成该语言的语法;这是您的惟一选择。然而,当您渐渐习惯使用允许轻松扩展的语言时,您就会开始将语言折向解决问题的方向,而不是其他折衷的方式。

Java 缺乏直观的语言扩展机制,除非您求助于面向方面的编程。然而,下一代的 JVM 语言(Groovy、Scala 和 Clojure)(参阅
参考资料)均支持以多种方式进行扩展。通过这样做,它们可以达到解释器设计模式的目的。首先,我将展示如何使用这三种语言实现操作符重载,然后演示 Groovy 和 Scala 如何让您扩展现有的类。

操作符重载(operator overloading)

操作符重载 是函数式语言的一个常见特性,能够重定义操作符(如
+
-
*
)配合新的类型工作,并表现出新的行为。操作符重载的缺失是 Java 形成时期的一个有意识的决定,但现在几乎每一个现代语言都具备这个特性,包括在 JVM 上 Java 的天然接班人。

Groovy

Groovy 尝试更新 Java 的语法,使其跟上潮流,同时保留其自然语义。因此,Groovy 通过将操作符自动映射到方法名称实现操作符重载。例如,如果您想重载
Integer
+
操作符,那么您要重写
Integer
类的
plus()
方法。完整的映射列表已在线提供(参阅

参考资料);表 1 显示了列表的一部分:

表 1. Groovy 的操作符/方法映射列表的一部分
操作符方法
x + y
x.plus(y)
x * y
x.multiply(y)
x / y
x.div(y)
x ** y
x.power(y)
作为一个操作符重载的示例,我将在 Groovy 和 Scala 中都创建一个
ComplexNumber
类。复数 是一个数学概念,由一个实数 和虚数 部分组成,一般写法是,例如
3 + 4i
。复数在许多科学领域中都很常用,包括工程学、物理学、电磁学和混沌理论。开发人员在编写这些领域的应用程序时,大大受益于能够创建反映其问题域的操作符。(有关复数的更多信息,请参阅

参考资料。)

清单 1 中显示了一个 Groovy
ComplexNumber
类:

清单 1. Groovy 中的
ComplexNumber


package complexnums

class ComplexNumber {
def real, imaginary

public ComplexNumber(real, imaginary) {
this.real = real
this.imaginary = imaginary
}

def plus(rhs) {
new ComplexNumber(this.real + rhs.real, this.imaginary + rhs.imaginary)
}

def multiply(rhs) {
new ComplexNumber(
real * rhs.real - imaginary * rhs.imaginary,
real * rhs.imaginary + imaginary * rhs.real)
}

String toString() {
real.toString() + ((imaginary < 0 ? "" : "+") + imaginary + "i").toString()
}
}



清单 1 中,我创建一个类,保存实数和虚数部分,并且我创建重载的
plus()
multiply()
操作符。两个复数的相加是非常直观的:
plus()
操作符将两个数各自的实数和虚数分别进行相加,并产生结果。两个复数的相乘需要以下公式:

(x + yi)(u + vi) = (xu - yv) + (xv + yu)i



清单 1 中的
multiply()
操作符复制该公式。它将两个数字的实数部分相乘,然后减去虚数部分相乘的积,再加上实数和虚数分别彼此相乘的积。

清单 2 测试复数运算符:

清单 2. 测试复数运算符

package complexnums

import org.junit.Test
import static org.junit.Assert.assertTrue
import org.junit.Before

class ComplexNumberTest {
def x, y

@Before void setup() {
x = new ComplexNumber(3, 2)
y = new ComplexNumber(1, 4)
}

@Test void plus_test() {
def z = x + y;
assertTrue 3 + 1 == z.real
assertTrue 2 + 4 == z.imaginary
}

@Test void multiply_test() {
def z = x * y
assertTrue(-5  == z.real)
assertTrue 14 == z.imaginary
}
}



清单 2 中,
plus_test()
multiply_test()
方法对重载操作符的使用(两者都以该领域专家使用的相同符号代表)与类似的内置类型用法没什么区别。

Scala(和 Clojure)

Scala 通过放弃操作符和方法之间的区别来实现操作符重载:操作符仅仅是具有特殊名称的方法。因此,要使用 Scala 重写乘法运算,您要重写
*
方法。在清单 3 中,我用 Scala 创建复数。

清单 3. Scala 中的复数

class ComplexNumber(val real:Int, val imaginary:Int) {
def +(operand:ComplexNumber):ComplexNumber = {
new ComplexNumber(real + operand.real, imaginary + operand.imaginary)
}

def *(operand:ComplexNumber):ComplexNumber = {
new ComplexNumber(real * operand.real - imaginary * operand.imaginary,
real * operand.imaginary + imaginary * operand.real)
}

override def toString() = {
real + (if (imaginary < 0) "" else "+") + imaginary + "i"
}
}


清单 3 中的类包括熟悉的
real
imaginary
成员,以及
+
*
操作符/方法。如清单 4 所示,我可以自然地使用
ComplexNumber


清单 4. 在 Scala 中使用复数

val c1 = new ComplexNumber(3, 2)
val c2 = new ComplexNumber(1, 4)
val c3 = c1 + c2
assert(c3.real == 4)
assert(c3.imaginary == 6)

val res = c1 + c2 * c3

printf("(%s) + (%s) * (%s) = %s\n", c1, c2, c3, res)
assert(res.real == -17)
assert(res.imaginary == 24)


通过统一操作符和方法,Scala 使操作符重载变成一件小事。Clojure 使用相同的机制来重载操作符。例如,以下 Clojure 代码定义了一个重载的
**
操作符:

(defn ** [x y] (Math/pow x y))


回页首

扩展类

类似于操作符重载,下一代的 JVM 语言允许您扩展类(包括核心 Java 类),扩展的方式在 Java 语言本身是不可能实现的。这些设施通常用于构建领域特定的语言 (DSL)。虽然 GOF 从来没有考虑过 DSL(因为它们与当时流行的语言没有共同点),DSL 却体现了解释器设计模式的初衷。

通过将计量单位和其他修饰符添加给
Integer
等核心类,您可以(就像添加操作符一样)更紧密地对现实问题进行建模。Groovy 和 Scala 都支持这样做,但它们使用不同的机制。

Groovy 的
Expando
和类别类

Groovy 包括两种对现有类添加方法的机制:
ExpandoMetaClass
和类别。(在
函数式思维:函数设计模式,第 2 部分 中,我在适配器模式的上下文中详细介绍过
ExpandoMetaClass
。)

比方说,您的公司由于离奇的遗留原因,需要以浪(furlongs,英国的计量单位)/每两周而不是以英里/每小时 (MPH) 的方法来表达速度,开发人员发现自己经常要执行这种转换。使用 Groovy 的 
ExpandoMetaClass
,您可以添加一个
FF
属性给处理转换的
Integer
,如清单 5 所示:

清单 5. 使用
ExpandoMetaClass
添加一个浪/两周的计量单位给
Integer


static {
Integer.metaClass.getFF { ->
delegate * 2688
}
}

@Test void test_conversion_with_expando() {
assertTrue 1.FF == 2688
}


ExpandoMetaClass
的替代方法是,创建一个类别 包装器类,这是从 Objective-C 借来的概念。在清单 6 中,我添加了一个(小写)
ff
属性给
Integer


清单 6. 通过一个类别类添加计量单位

classFFCategory {
static Integer getFf(Integer self) {
self * 2688
}
}

@Test void test_conversion_with_category() {
use(FFCategory) {
assertTrue 1.ff == 2688
}
}


一个类别类是一个带有一组静态方法集合的普通类。每个方法接受至少一个参数;第一个参数是这种方法增强的类型。例如,在
清单 6 中,
FFCategory
类拥有一个
getFf()
方法,它接受一个
Integer
参数。当这个类别类与
use
关键字一起使用时,代码块内所有相应类型都被增强。在单元测试中,我可以在代码块内引用
ff
属性(记住,Groovy 自动将
get
方法转换为属性引用),如在
清单 6 的底部所示。

有两种机制可供选择,让您可以更准确地控制增强的范围。例如,如果整个系统使用 MPH 作为速度的默认单位,但也需要频繁转换为浪/每两周,那么使用
ExpandoMetaClass
进行全局修改将是适当的。

您可能对重新开放核心 JVM 类的有效性持怀疑态度,担心会产生广泛深远的影响。类别类让您限制潜在危险性增强的范围。以下是一个来自真实世界的开源项目示例,它极好地利用了这一机制。

easyb 项目(参阅
参考资料)让您可以编写测试,以验证正接受测试的类的各个方面。请研究清单 7 所示的 easyb 测试代码片段:

清单 7. easyb 测试一个
queue


it "should dequeue items in same order enqueued", {
[1..5].each {val ->
queue.enqueue(val)
}
[1..5].each {val ->
queue.dequeue().shouldBe(val)
}
}


queue
类不包括
shouldBe()
方法,这是我在测试的验证阶段所调用的方法。easyb 框架已为我添加了该方法;清单 8 中所显示的在 easyb 源代码中的
it()
方法定义,演示了该过程:

清单 8. easyb 的
it()
方法定义


def it(spec, closure) {
stepStack.startStep(BehaviorStepType.IT, spec)
closure.delegate = new EnsuringDelegate()
try {
if (beforeIt != null) {
beforeIt()
}
listener.gotResult(new Result(Result.SUCCEEDED))
use(categories) {
closure()
}
if (afterIt != null) {
afterIt()
}
} catch (Throwable ex) {
listener.gotResult(new Result(ex))
} finally {
stepStack.stopStep()
}
}

class BehaviorCategory {
// ...

static void shouldBe(Object self, value) {
shouldBe(self, value, null)
}

//...
}



清单 8中,
it()
方法接受了一个 spec (描述测试的一个字符串)和一个代表测试的主体的闭包块。在方法的中间,闭包会在
BehaviorCategory
块内执行,该块出现在清单的底部。
BehaviorCategory
增强
Object
,允许 Java 世界中的任何 实例验证其值。

通过允许选择性增强驻留在层次结构顶层的
Object
,Groovy 的开放类机制可以轻松地实现为任何实例验证结果,但它限制了对
use
块主体的修改。

Scala 的隐式转换

Scala 使用隐式转换 来模拟现有类的增强。隐式转换不会对类添加方法,但允许语言自动将一个对象转换成拥有所需方法的相应类型。例如,我不能将
isBlank()
方法添加到
String
类中,但我可以创建一个隐式转换,将
String
自动转换为拥有这种方法的类。

作为一个示例,我想将
append()
方法添加到
Array
,这让我可以轻松地将
Person
实例添加到适当类型的数组,如清单 9 所示:

清单 9.将一个方法添加到
Array
中,以增加人员


case class Person (firstName: String, lastName: String) {}

class PersonWrapper(a: Array[Person]) {
def append(other: Person) = {
a ++ Array(other)
}
def +(other: Person) = {
a ++ Array(other)
}
}

implicit def listWrapper(a: Array[Person]) = new PersonWrapper(a)



清单 9中,我创建一个简单的
Person
类,它带有若干个属性。为了使
Array[Person]
(在 Scala 中,一般使用
[ ]
而不是
< >
作为分隔符)
Person
可知,我创建一个
PersonWrapper
类,它包括所需的
append()
方法。在清单的底部,我创建一个隐式转换,当我在数组上调用
append()
方法时,隐式转换会自动将一个
Array[Person]
转换为
PersonWrapper
。清单 10 测试该转换:

清单 10. 测试对现有类的自然扩展

val p1 = new Person("John", "Doe")
var people = Array[Person]()
people = people.append(p1)



清单 9中,我也为
PersonWrapper
类添加了一个
+
方法。清单 11 显示了我如何使用操作符的这个漂亮直观的版本:

清单 11. 修改语言以增强可读性

people = people + new Person("Fred", "Smith")
for (p <- people)
printf("%s, %s\n", p.lastName, p.firstName)


Scala 实际上并未对原始的类添加一个方法,但它通过自动转换成一个合适的类型,提供了这样做的外观。使用 Groovy 等语言进行元编程所需要的相同工作在 Scala 中也需要,以避免过多使用隐式转换而产生由相互关联的类所组成的令人费解的网。但是,在正确使用时,隐式转换可以帮助您编写表达非常清晰的代码。

回页首

结束语

来自 GoF 的原始解释器设计模式建议创建一个新语言,但其基础语言并不支持我们今天所掌握的良好扩展机制。下一代 Java 语言都通过使用多种技术来支持语言级别的可扩展性。在本期文章中,我演示了操作符重载如何在 Groovy、Scala 和 Clojure 中工作,并研究了在 Groovy 和 Scala 中的类扩展。

在下期文章中,我将展示 Scala 风格的模式匹配和泛型的组合如何取代一些传统的设计模式。该讨论的中心是一个在函数式错误处理中也起着作用的概念,这一概念将是我们下期文章的主题。

参考资料

学习

The Productive Programmer(Neal Ford,O'Reilly Media,2008 年):Neal Ford 的新书讨论了帮助您提高编码效率的工具和实践。
Design Patterns: Elements of Reusable Object-Oriented Software(Erich
Gamma 等人,Addison-Wesley,1994 年):关于 Gang of Four 在设计模式方面的经典之作。
Complex number:复数是数学抽象,在许多科学领域中发挥作用。
Scala:Scala 是一种现代函数编程语言,适用于 JVM。
Clojure:Clojure 是一种现代函数式 Lisp,适用于 JVM。
Groovy:Groovy 是一种现代动态 JVM 语言,具有多种函数方面。
Operator overloading in Groovy:此页显示 Groovy 中支持的操作符以及其映射方法的完整列表。
"实战 Groovy: 使用闭包、ExpandoMetaClass 和类别进行元编程"(Scott Davis,developerWorks,2009 年 6 月):了解有关在 Groovy 中元编程的更多信息。
easyb:easyb 是一个使用 Groovy 开发的行为驱动开源开发工具,适用于 Groovy 和 Java 项目。
"Drive development with easyb"(Andrew Glover,developerWorks,2008 年 11 月):了解 easyb 如何帮助开发人员及利益相关者进行协作。
Grails:Grails 是使用 Java 和 Groovy 编写的一种开源 Web 框架。
Ruby on Rails:Rails 是使用 Ruby 编写的一种开源 Web 框架,运行于 JRuby 上。
浏览
技术书店,阅读有关这些主题和其他技术主题的图书。
developerWorks 中国网站 Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: