用 curry 过的闭包进行函数式编程

 

几乎从一年前实战 Groovy 系列开始,我就已经提供了多个让您了解闭包的机会。在首次作为 alt.lang.jre 系列的一部分写 Groovy 时(“感受 Groovy”,2004 年 8 月),我介绍了 Groovy 的闭包语法,而且 就在上一期文章中,我介绍了最新的 JSR 标准对相同语法的更新。学习至今,您知道了 Groovy 闭包是代码块,可以被引用、带参数、作为方法参数传递、作为返回值从方法调用返回。而且,它们也可以作为其他闭包的参数或返回值。因为闭包是 Closure 类型的对象,所以它们也可以作为类的属性或集合的成员。

虽然这些技术都是很神奇的,但是本期文章要学习的闭包技术要比您迄今为止尝试过的都要更火辣一些。客座作者 John Savage 和 Ken Barclay 已经用 Groovy 闭包中的 curry() 方法做了一些有趣的实验,我们非常有幸可以见识他们的技艺。

关于本系列

把任何一个工具集成进开发实践的关键是,知道什么时候使用它而什么时候把它留在箱子中。脚本语言可以是工具箱中极为强大的附件,但是只有在恰当地应用到适当的场景中时才是这样。为了这个目标,实战 Groovy 是一系列文章,专门介绍 Groovy 的实际应用,并教您何时以及如何成功地应用它们。

Barclay 和 Savage 的 curry 过的闭包不仅会重新激起您对熟悉的操作(例如复合)和 Visitor 设计模式的兴奋,还会用 Groovy 打开函数式编程的大门。

进行 curry 处理

curry 过的函数 一般可在函数式编程语言(例如 ML 和 Haskell)中找得到(请参阅 参考资料)。curry 这个术语来自 Haskell Curry,这个数学家发明了局部函数的概念。Currying 指的是把多个参数放进一个接受许多参数的函数,形成一个新的函数接受余下的参数,并返回结果。令人兴奋的消息是,Groovy 的当前版本(在编写本文时是 jsr-02 版)支持在闭包上使用 curry() 方法 —— 这意味着,我们这些 Groovy 星球的公民,现在可以利用函数式编程的某些方面了!

您以前可能从未用过 curry(),所以我们先从一个简单的、熟悉的基础开始。清单 1 显示了一个叫做 multiply 的闭包。它有形式参数 xy,并返回这两个值的乘积。假设没有歧义存在,然后代码演示了用于执行 multiply 闭包的两种方法:显式(通过 call 的方式)或隐式。后一种样式引起了函数调用方式。


清单 1. 简单的闭包
def multiply = { x,y -> return x * y } // closure
def p = multiply.call(3,4) // explicit call
def q = multiply(4,5) // implicit call
println "p: ${p}" // p is 12
println "q: ${q}" // q is 20

这个闭包当然很好,但是我们要对它进行 curry 处理。在调用 curry() 方法时,不需要提供所有的实际参数。curry 过的 调用只引起了闭包的部分应用程序。闭包的 部分应用程序 是另一个 Closure 对象,在这个对象中有些值已经被修正。

清单 2 演示了对 multiply 闭包进行 curry 处理的过程。在第一个例子中,参数 x 的值被设置为 3。名为 triple 的闭包现在有效地拥有了 triple = { y -> return 3 * y } 的定义。


清单 2. curry 过的闭包
def multiply = { x,y -> return x * y }  // closure
def triple = multiply.curry(3) // triple = { y -> return 3 * y }
def quadruple = multiply.curry(4)
// quadruple = { y -> return 4 * y }
def p = triple.call(4) // explicit call
def q = quadruple(5) // implicit call
println "p: ${p}" // p is 12
println "q: ${q}" // q is 20

可以看到,参数 x 已经从 multiply 的定义中删除,所有它出现的地方都被 3 这个值代替了。

curry 过的数学 101

从基本数学可能知道,乘法运算符是可交换的(换句话说 x * y = y * x)。但是,减法运算符是不可交换的;所以,需要两个操作来处理减数和被减数。清单 3 为这个目的定义了闭包 lSubtractrSubtract(分别在左边和右边),结果显示了 curry 函数的一个有趣的应用。


清单 3. 右和右操作数
def lSubtract = { x,y -> return x - y }
def rSubtract = { y,x -> return x - y }
def dec = rSubtract.curry(1)
// dec = { x -> return x - 1 }
def cent = lSubtract.curry(100)
// cent = { y -> return 100 - y }
def p = dec.call(5) // explicit call
def q = cent(25) // implicit call
println "p: ${p}" // p is 4
println "q: ${q}" // q is 75







迭代和复合

您会回忆起这个系列以前的文章中,闭包一般用于在 ListMap 集合上应用的迭代器方法 上。例如,迭代器方法 collect 把闭包应用到集合中的每个元素上,并返回一个带有新值的新集合。清单 4 演示了把 collect 方法应用于 ListMap。名为 agesList 被发送给 collect() 方法,使用单个闭包 { element -> return element + 1 } 作为参数。注意方法的最后一个参数是个闭包,在这个地方 Groovy 允许把它从实际参数列表中删除,并把它直接放在结束括号后面。在没有实际参数时,可以省略括号。用名为 accountsMap 对象调用的 collect() 方法可以展示这一点。


清单 4. 闭包和集合
def ages = [20,30,40]
def accounts = ['ABC123' : 200,'DEF456' : 300,'GHI789' : 400]
def ages1 = ages.collect({ element -> return element + 1 })
def accounts1 = accounts.collect
{ entry -> entry.value += 10; return entry }
println "ages1: ${ages1}"
// ages1: [21,31,41]
println "accounts1: ${accounts1}"
// accounts1: [ABC123=210,GHI789=410,DEF456=310]
def ages2 = ages.collect { element -> return dec(element) }
println "ages2: ${ages2}"
// ages2: [19,29,39]

最后一个例子搜集名为 agesList 中的所有元素,并将 dec 闭包 (来自清单 3)应用于这些元素。

闭包复合

闭包更重要的一个特征可能就是复合(composition),在复合中可以定义一个闭包,它的目的是组合其他闭包。使用复合,两个或多个简单的闭包可以组合起来构成一个更复杂的闭包。

清单 5 介绍了一个漂亮的 composition 闭包。现在请注意仔细阅读:参数 fg 代表 单个参数闭包。到现在还不错?现在,对于某些参数值 x,闭包 g 被应用于 x,而闭包 f 被应用于生成的结果!哦,只对前两个闭包参数进行了 curry 处理,就有效地形成了一个组合了这两个闭包的效果的新闭包。

清单 5 组合了闭包 triplequadruple,形成闭包 twelveTimes。当把这个闭包应用于实际参数 3 时,返回值是 36。


清单 5. 超级闭包复合
def multiply = { x,y -> return x * y }    
// closure
def triple = multiply.curry(3)
// triple = { y -> return 3 * y }
def quadruple = multiply.curry(4)
// quadruple = { y -> return 4 * y }
def composition = { f,g,x -> return f(g(x)) }
def twelveTimes = composition.curry(triple,quadruple)
def threeDozen = twelveTimes(3)
println "threeDozen: ${threeDozen}"
// threeDozen: 36

非常漂亮,是么?







五星计算

现在我们来研究闭包的一些更刺激的方面。我们先从一个机制开始,用这个机制可以表示包含计算模式 的闭包,计算模式是一个来自函数式编程的概念。计算模式的一个例子就是用某种方式把 List 中的每个元素进行转换。因为这些模式发生得如此频繁,所以我们开发了一个叫做 Functor 的类,把它们封装成 static Closure。清单 6 显示了一个摘要。


清单 6. Functor 封装了一个计算模式
package fp
abstract class Functor {
// arithmetic (binary,left commute and right commute)
public static Closure bMultiply = { x,y -> return x * y }
public static Closure rMultiply = { y,x -> return x * y }
public static Closure lMultiply = { x,y -> return x * y }

// ...
// composition
public static Closure composition = { f,x -> return f(g(x)) }

// lists
public static Closure map =
{ action,list -> return list.collect(action) }

public static Closure apply = { action,list -> list.each(action) }

public static Closure forAll = { predicate,list ->
for(element in list) {
if(predicate(element) == false) {
return false
}
}
return true
}
// ...
}

在这里可以看到名为 map 的闭包,不要把它与 Map 接口混淆。map 闭包有一个参数 f 代表闭包,还有一个参数 list 代表(不要惊讶)List。它返回一个新 List,其中 f 已经映射到 list 中的每个元素。当然,Groovy 已经有了用于 Listscollect() 方法,所以我们在我们的实现中也使用了它。

在清单 7 中,我们把事情又向前进行了一步,对 map 闭包进行了 curry 处理,形成一个块,会将指定列表中的所有元素都乘以 12。


清单 7. 添加一些 curry,并乘以 12
import fp.*
def twelveTimes = { x -> return 12 * x }
def twelveTimesAll = Functor.map.curry(twelveTimes)
def table = twelveTimesAll([1,2,3,4])
println "table: ${table}"
// table: [12,24,36,48]

现在, 就是我们称之为五星计算的东西!







业务规则用 curry 处理

关于闭包的技艺的讨论很不错,但是更关注业务的人会更欣赏下面这个例子。在考虑计算特定 Book 条目净值的问题时,请考虑商店的折扣和政府的税金(例如 增值税)。如果想把这个逻辑作为 Book 类的一部分包含进来,那么形成的解决方案可能是个难缠的方案。因为书店可能会改变折扣,或者折扣只适用于选定的存货,所以这样一个解决方案可能会太刚性了。

但是猜猜情况如何。变化的业务规则非常适合于使用 curry 过的闭包。可以用一组简单的闭包来表示单独的业务规则,然后用复合把它们以不同的方式组合起来。最后,可以用 计算模式 把它们映射到集合。

清单 8 演示了书店的例子。闭包 rMultiply 是个局部应用程序,通过使用一个不变的第二操作数,把二元乘法改变成一元闭包。两个图书闭包 calcDiscountedPricecalcTaxrMultiply 闭包的实例,为乘数值设置了值。闭包 calcNetPrice 是计算净价的算法:先计算折扣价格,然后在折扣价格上计算销售税。最后, calcNetPrice 被应用于图书价格。


清单 8. 图书业务对象
import fp.*
class Book {
@Property name
@Property author
@Property price
@Property category
}
def bk = new Book(name : 'Groovy',author :
'KenB',price : 25,category : 'CompSci')
// constants
def discountRate = 0.1
def taxRate = 0.17
// book closures
def calcDiscountedPrice = Functor.rMultiply.curry(1 - discountRate)
def calcTax = Functor.rMultiply.curry(1 + taxRate)
def calcNetPrice =
Functor.composition.curry(calcTax,calcDiscountedPrice)
// now calculate net prices
def netPrice = calcNetPrice(bk.price)
println "netPrice: ${netPrice}" // netPrice: 26.325








更加 groovy 的访问者

已经看到了如何把 curry 过的闭包应用于函数模式,所以现在我们来看看在使用相似的技术重新访问重要的面向对象设计模式时发生了什么。对于面向对象系统来说,必须遍历对象集合并在 集合中的每个元素上执行某个操作,是非常普通的使用情况。假设一个不同的场景,系统要遍历相同的集合,但是要执行不同的操作。通常,需要用 Visitor 设计模式(请参阅 参考资料)来满足这一需求。Visitor 接口引入了处理集合元素的动作协议。具体的子类定义需要的不同行为。方法被引进来遍历集合并对每个元素应用 Visitor 动作。

如果到现在您还没猜出来,那么可以用闭包实现同的效果。这种方法的一个抽象是:使用闭包,不需要开发访问者类的层次结构。而且,可以有效地使用闭包复合和映射来定义集合的动作和效果遍历。

例如,考虑用来表示图书馆库存的类 Library 和类 Book 之间的一对多关系。可以用 ListMap 实现这个关系;但是 Map 提供的优势是它能提供快速的查询,也就是说用图书目录编号作为键。

清单 9 显示了一个使用 Map 的简单的一对多关系。请注意 Library 类中的两个显示方法。引入访问者时,两者都是重构的目标。


清单 9. 图书馆应用程序
class Book {    
@Property title
@Property author
@Property catalogNumber
@Property onLoan = false
String toString() {
return "Title: ${title}; author: ${author}"
}
}
class Library {
@Property name
@Property stock = [ : ]

def addBook(title,author,catalogNumber) {
def bk = new Book(title : title,author :
author,catalogNumber : catalogNumber)
stock[catalogNumber] = bk
}

def lendBook(catalogNumber) {
stock[catalogNumber].onLoan = true
}

def displayBooksOnLoan() {
println "Library: ${name}"
println "Books on loan"
stock.each { entry ->
if(entry.value.onLoan == true) println entry.value
}
}

def displayBooksAvailableForLoan() {
println "Library: ${name}"
println "Books available for loan"
stock.each { entry ->
if(entry.value.onLoan == false) println entry.value
}
}
}
def lib = new Library(name : 'Napier')
lib.addBook('Groovy','KenB',
'CS123')
lib.addBook('Java','JohnS','CS456')
lib.addBook('UML','Ken and John',
'CS789')
lib.lendBook('CS123')
lib.displayBooksOnLoan() // Title: Groovy; author: KenB
lib.displayBooksAvailableForLoan() // Title: UML; author: Ken and John
// Title: Java; author: JohnS

清单 10 包含 Library 类中的几个闭包,模拟了访问者的用法。action 闭包(与 map 闭包有点相似)把 action 闭包应用于 List 的每个元素。如果某本书被借出,则闭包 displayLoanedBook 显示它;如果某本书未被借出,则闭包 displayAvailableBook 显示它。两者都扮演访问者和相关的动作。用 displayLoanedBookapply 闭包进行 curry 处理,会形成闭包 displayLoanedBooks,它为处理图书集合做好了准备。类似的方案也用来生成可供借阅的图书显示,如清单 10 所示。


清单 10. 修订后的图书馆访问者
import fp.*
class Book {
@Property title
@Property author
@Property catalogNumber
@Property onLoan = false
String toString() {
return " Title: ${title}; author: ${author}"
}

}
class Library {
@Property name
@Property stock = [ : ]
def addBook(title,catalogNumber : catalogNumber)
stock[catalogNumber] = bk
}

def lendBook(catalogNumber) {
stock[catalogNumber].onLoan = true
}

def displayBooksOnLoan() {
println "Library: ${name}"
println "Books on loan"
displayLoanedBooks(stock.values())
}

def displayBooksAvailableForLoan() {
println "Library: ${name}"
println "Books available for loan"
displayAvailableBooks(stock.values())
}


private displayLoanedBook = { bk -> if(bk.onLoan == true)
println bk }
private displayAvailableBook = { bk -> if(bk.onLoan == false)
println bk }

private displayLoanedBooks =
Functor.apply.curry(displayLoanedBook)
private displayAvailableBooks =
Functor.apply.curry(displayAvailableBook)
}
def lib = new Library(name : 'Napier')
lib.addBook('Groovy',
'CS789')
lib.lendBook('CS123')
lib.displayBooksOnLoan() // Title: Groovy; author: KenB
lib.displayBooksAvailableForLoan() // Title: UML; author: Ken and John
// Title: Java; author: JohnS








用闭包进行测试

在结束之前,我们来看一下 Groovy 闭包的一个附加用途。请考虑一个被建模为具有许多 EmployeeCompany 的应用程序。递归的关系在单个 Employee(团队领导)和许多 Employee(团队成员)之间进一步建立起一对多的聚合。图 1 是这样一个组织的类图。


图 1. Company 应用程序

Company 应用程序的类图


可以用闭包来表述模型架构上的完整性。例如,在这个例子中,可能想确保每个员工都分配了一个经理。简单的闭包 hasManager 为每个员工表达了这个需求: def hasManager = { employee -> return (employee.manager != null) }

来自清单 6 的 Functor 类中的 forAll 闭包的局部应用程序能够描述架构的需求:def everyEmployeeHasManager = Functor.forAll.curry(hasManager)

清单 11 演示了 curry 过的闭包的应用:测试系统架构的完整性。


清单 11. 用于测试架构完整性的闭包
import fp.*
/**
* A company with any number of employees.
* Each employee is responsible
* to a team leader who,in turn,manages a team of staff.
*/
import java.util.*
class Employee {
@Property id
@Property name
@Property staff = [ : ]
@Property manager = null
String toString() {
return "Employee: ${id} ${name}"
}

def addToTeam(employee) {
staff[employee.id] = employee
employee.manager = this
}

}
class Company {
@Property name
@Property employees = [ : ]
def hireEmployee(employee) {
employees[employee.id] = employee
}

def displayStaff() {
println "Company: ${name}"
println "===================="
employees.each { entry -> println "
${entry.value}" }
}


}
def co = new Company(name : 'Napier')
def emp1 = new Employee(id : 123,name : 'KenB')
def emp2 = new Employee(id : 456,name : 'JohnS')
def emp3 = new Employee(id : 789,name : 'JonK')
co.hireEmployee(emp1)
co.hireEmployee(emp2)
co.hireEmployee(emp3)
emp3.addToTeam(emp1)
emp3.addToTeam(emp2)
co.displayStaff()
// Architectural closures
def hasManager = { employee -> return (employee.manager != null) }
def everyEmployeeHasManager = Functor.forAll.curry(hasManager)
def staff = new ArrayList(co.employees.values())
println "Every employee has a manager?:
${everyEmployeeHasManager.call(staff)}" // false







curry 是优秀的

在本文中您已看到了大量闭包,但是希望能激起您对更多闭包的渴望。在学习乘法例子时,curry 过的闭包使得实现计算的函数模式出奇得容易。一旦掌握了这些模式,就可以把它们部署到常见的企业场景中,例如我们在书店例子中把它们应用于业务规则。把闭 包应用于函数模式是令人兴奋的,一旦这么做了之后,再把它们应用于面向对象设计模式,就不是什么大事情了。Curry 过的闭包可以用来模拟 Visitor 模式的基本元素,正如在 Library 例子中显示的。它们在软件测试期间执行完整性测试时也会有用,就像用 Company 例子所展示的。

本文中看到的全部例子都是企业系统常见的用例。看到 Groovy 闭包和 curry 方法能够如此流畅地应用于众多编程场景、函数模式和面向对象模式,真是激动人心。Haskell Curry 肯定发现了这个可怕的 groovy

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


背景:    8月29日,凌晨4点左右,某服务告警,其中一个节点直接down掉,收到告警的同事让运维重启。    9点左右,内存监控上发现内存异常,堆内存涨速很快,即便GC也没有什么效果,频繁GC。    9点38,服务各种超时,影响整个app使用。处理方式:    当时由于很想要
https://support.smartbear.comeadyapi/docs/soapui/steps/groovy.htmlGettestcaseobjectToobtaintheobjectwhichreferstothecontainingtestcase,usethefollowingcodesnippet:Groovy def case=testRunner.testCase; Byusingthe tes
有几个选项可用于执行自定义JMeter脚本并扩展基线JMeter功能。查看最流行的扩展机制,比较性能并分析哪一个是最好的。  这是乐队之战,JMeter风格。 BeanshellV.JSR223V.JavaRequestSampler 在我们之前的帖子中,  JMeterPerformance和TuningTips  ( 由fantastik
Scala和Java为静态语言,Groovy为动态语言Scala:函数式编程,同时支持面向对象Groovy:jvm上的脚本,较好兼容java语法,Groovy加强了Java集成。 可配置化的优势,可以将一些简单的逻辑公开给外部编辑和使用,增强了互操作性,复杂逻辑来说,可配置化代码的调试则会比较麻烦 Scala和Java
出处:https://www.jianshu.com/p/ce6f8a1f66f4一、一些内部元件的访问testRunner.testCase开头1、向下访问testRunner.testCase.testSteps[testStepName]testRunner.testCase.getTestStepByName("新增一个空间")2、向上访问,用于访问同一项目中的其他testSuites和testCase下
在运行groovy的junit方法时,报了这个错误:java.lang.ExceptionInInitializerError atorg.codehaus.groovy.reflection.ClassInfo.isValidWeakMetaClass(ClassInfo.java:271) atorg.codehaus.groovy.reflection.ClassInfo.getMetaClassForClass(ClassInfo.java:241) atorg.codeha
基本语法1.Grovvy的注释分为//和/**/和java的一样.2.Grovvy语法可以不已分号结尾.3.单引号,里面的内容严格的对应java中的String,不对$符号进行转义.defs1='iamastudent$'printlns1iamastudent$4.双引号“”的内容中如果有$号的话,会先对表达式先求值.de
Tiobe发布了最新一期(3月份)编程语言欢迎度榜单,其榜单根据互联网上有经验的程序员、课程和第三方厂商的数量,并使用搜索引擎(如Google、Bing、Yahoo!)以及Wikipedia、Amazon、YouTube统计出排名数据。TOP5几乎没有变化,Java和C语言牢牢占据前两名。Python相较去年上升一位进入TOP3,C++下
我有一个Google地图组件,作者可以在其中指定纬度和经度.我正在使用带有正则表达式的常规“输入”类型控件来验证它们是否是数字,但是,当试图解决指定范围的问题时(经度验证该值在[-180,180]内并且纬度[-90,90])但是,通过正则表达式进行验证似乎很麻烦,而且利用inputtype=“numb
我正在为未来的应用程序评估SpringBoot,并希望使用Groovy模板来实现其纯粹的可读性.不幸的是,我在迭代我添加到控制器返回的ModelAndView对象的对象列表时遇到了麻烦.这是我的控制器:@RestController@RequestMapping("/ships")publicclassShipsController{@Autowired
我有一个基于Spring的java应用程序,其中包含一些有用的组件.作为系统的一部分,我有一个groovy脚本,来处理一些报告.我想从groovy脚本中调用spring组件.当我用Java编写时,我需要在@Component中使用@Autowired注释,即@ComponentclassReporter{@AutowiredSearchServicesearchS
在Grailsi18n插件definedthusly中定义了一个messageSourcebean:messageSource(PluginAwareResourceBundleMessageSource){basenames=baseNames.toArray()fallbackToSystemLocale=falsepluginManager=manager....}是否可以覆盖我的resources.groovy中的fa
我正在寻找一种方法来反向工程RDBMS表(MSSQLServer)并生成JPA@EntityGroovy类.我们目前没有选择使用Grails和/或GORM,因此Grailsdb-reverse-engineer插件似乎很接近但不太正确.它生成符合GORM的类而不是JPA实体类.我们目前有一个gradle构建,它利用org.hibernate.tool.ant.Hibe
https://blog.csdn.net/Gdeer/article/details/83062523一、直接运行groovy程序因为groovy插件和android插件不兼容,所以不能在原始项目上使用groovy。 新建module,创一个JavaLibrary,取名lib。  修改lib/build.gradleapplyplugin:'java-library'depe
一、自动生成GET请求脚本1、配置Createascript在ngrinder管理台主页,点击script–>Createascript,并填写脚本名称和请求的url,如下所示:点击Create按钮,nGrinder会自动生成对应的脚本结构,如果没有参数需要设置的话,可以直接运行了。二、详细解析GET请求脚本ngrinder自动生成的脚本
我正在关注使用列表和地图作为构造函数的this博文.为什么以下列表无法强制反对?classTest{staticclassTestObject{privateinta=1;protectedintb=2;publicintc=3;intd=4;Strings="s";}stati
Information:java:Errorsoccurredwhilecompilingmodule'security'Information:javac1.8.0_131wasusedtocompilejavasourcesInformation:2019/6/98:31-Buildcompletedwith1errorand0warningsin3s116msError:java:读取E:\repository\org
ngrinder中的groovy脚本结构类似junit,同时在junit的基础之上封装了自己的注解,用来控制脚本的运行。一、运行逻辑图如下:此处只列出了groovy脚本的逻辑,jython脚本是类似的,在此不再单独介绍。二、各注解的使用比较三、关注点在ngrinder中,通常使用单进程多线程就足够大部分测试了,所以:
我有一个switch语句来处理javaenumfoo,并使用spock编写一些groovy单元测试.我已经添加了一个测试,它验证当前是否处理了每种类型的foo而没有抛出异常.现在我想测试一个无法识别的foo类型会导致抛出异常.要做到这一点,我将不得不嘲笑枚举,并已经看到这里概述的解决方案:MockingJ
我有一个groovy实体ClientInvoiceAttachmentExt,它扩展了java实体ClientInvoiceAttachment.ClientInvoiceAttachment具有@Id注释,但仍然看到“没有为实体指定的标识符”错误这是我的堆栈跟踪[Mar0317:11:54]ERROR|org.springframework.web.context.ContextLoader|Contex