咖喱和部分应用

    技术2024-02-18  57

    咖喱和部分应用是源自数学的语言技术(基于20世纪数学家Haskell Curry等人的工作)。 这些技术以各种类型的语言存在,并且其中之一或两者在功能语言中不存在。 通常,通过为某些参数提供一个或多个默认值(称为固定参数),可使用currying应用程序和部分应用程序使您能够操纵函数或方法的参数数量。 所有Java.next语言都包括currying和部分应用程序,但是它们以不同的方式实现它们。 在本期中,我将解释这两种技术之间的区别,并展示Scala,Groovy和Clojure的实现细节以及实际用法。

    有关术语的说明

    为此, 方法和功能可以互换。 支持currying和部分应用程序使用方法的面向对象语言。 同样, 函数参数和函数参数可以互换。 由于这些概念起源于数学,因此我始终使用函数和参数 ,但这并不意味着这些技术不适用于方法。

    定义和区别

    关于本系列

    Java™的遗产将是平台,而不是语言。 JVM上运行着200多种语言,不可避免的是,其中之一最终将取代Java语言,成为对JVM进行编程的最佳方法。 本系列探讨了三种下一代JVM语言:Groovy,Scala和Clojure,对新功能和范例进行了比较和对比,以使Java开发人员可以一窥他们不久的将来。

    对于不经意的观察者来说,弯曲和部分应用似乎具有相同的效果。 通过两者,您可以为某些参数创建具有预先提供的值的函数版本:

    Curinging描述了将多参数函数转换为单参数函数链。 它描述了转换过程,而不是转换函数的调用。 调用者可以决定要应用多少个参数,从而创建具有较少数量参数的派生函数。 部分应用描述了将多参数函数转换为接受较少参数的函数,并带有预先提供的省略参数的值。 该技术的名称很贴切:它将部分参数应用于函数,并返回带有签名的函数,该签名包含其余参数。

    对于临时应用程序和部分应用程序,您都需要提供参数值并返回缺少参数的可调用函数。 但是,使用函数会返回链中的下一个函数,而部分应用程序会将参数值绑定到您在操作期间提供的值,从而产生具有较小Arity (参数数量)的函数。 当您考虑Arity大于2的函数时,这种区别会变得更加清晰。 例如, process(x, y, z)函数的完全咖喱版本是process(x)(y)(z) ,其中process(x)和process(x)(y)都是接受单个函数论据。 如果仅咖喱第一个参数,则process(x)的返回值是一个接受单个参数,然后又接受单个参数的函数。 相反,使用部分应用程序后,您将获得较小的功能。 对process(x, y, z)的单个参数使用部分应用程序会产生一个接受两个参数的函数: process(y, z) 。

    两种技术的结果通常是相同的,但是区别很重要,而且常常会被误解。 为了使问题复杂化。此外,Groovy的同时实现了部分应用和柯里,但同时呼吁curry ING。 Scala同时具有部分应用的函数和PartialFunction ,尽管名称相似,但它们是不同的概念。

    在斯卡拉

    Scala支持currying和部分应用程序,以及一种特性,使您能够定义受约束的函数。

    咖喱

    在Scala中,函数可以将多个参数列表定义为括号集。 当调用的函数数量少于其定义数量时,返回的函数就是将缺少的参数列表作为其参数。 考虑清单1中出现的Scala文档中的示例。

    清单1. Scala的参数传递
    def filter(xs: List[Int], p: Int => Boolean): List[Int] = if (xs.isEmpty) xs else if (p(xs.head)) xs.head :: filter(xs.tail, p) else filter(xs.tail, p) def modN(n: Int)(x: Int) = ((x % n) == 0) val nums = List(1, 2, 3, 4, 5, 6, 7, 8) println(filter(nums, modN(2))) println(filter(nums, modN(3)))

    在清单1中, filter()函数递归地应用传递的过滤条件。 modN()函数由两个参数列表定义。 当我使用filter()调用modN ,我传递了一个参数。 filter()函数接受带有Int参数和Boolean返回Boolean的函数作为其第二个参数,该函数与我传递的curried函数的签名匹配。

    部分应用的功能

    在Scala中,您还可以部分应用函数,如清单2所示。

    清单2.在Scala中部分应用函数
    def price(product : String) : Double = product match { case "apples" => 140 case "oranges" => 223 } def withTax(cost: Double, state: String) : Double = state match { case "NY" => cost * 2 case "FL" => cost * 3 } val locallyTaxed = withTax(_: Double, "NY") val costOfApples = locallyTaxed(price("apples")) assert(Math.round(costOfApples) == 280)

    在清单2中,我首先创建一个price函数,该函数返回产品和价格之间的映射。 然后,我创建一个withTax()函数,该函数接受cost和state作为参数。 但是,在一个特定的源文件中,我知道我将专门处理一个州的税收。 我没有每次调用都“携带”额外的参数,而是部分地应用了state参数并返回了其中状态值固定的函数版本。 locallyTaxed函数接受单个参数cost 。

    部分(约束)功能

    Scala PartialFunction特征旨在与模式匹配无缝地工作。 (请参阅我的“ 功能性思考”系列的“ 任意树和模式匹配 ”部分中的模式匹配 。)尽管名称相似,但是此特征不会创建部分应用的功能。 相反,您可以使用它来定义仅适用于已定义的值和类型子集的函数。

    案例块是应用部分功能的一种方法。 清单3使用Scala的case而没有传统的对应match运算符。

    清单3.没有匹配的用case
    val cities = Map("Atlanta" -> "GA", "New York" -> "New York", "Chicago" -> "IL", "San Francsico " -> "CA", "Dallas" -> "TX") cities map { case (k, v) => println(k + " -> " + v) }

    在清单3中,我创建了城市和州对应关系的地图。 然后,我在集合上调用map函数,然后map依次拉开键-值对以打印它们。 在Scala中,包含case语句的代码块是定义匿名函数的一种方法。 您可以不使用case更简洁地定义匿名函数,但是case语法提供了清单4所示的其他好处。

    清单4. map和collect之间的区别
    List(1, 3, 5, "seven") map { case i: Int ? i + 1 } // won't work // scala.MatchError: seven (of class java.lang.String) List(1, 3, 5, "seven") collect { case i: Int ? i + 1 } // verify assert(List(2, 4, 6) == (List(1, 3, 5, "seven") collect { case i: Int ? i + 1 }))

    在清单4中,我不能在case对异构集合使用map :当函数尝试增加seven字符串时,我收到MatchError 。 但是collect工作正常。 为什么差异和错误出在哪里?

    案例块定义部分功能,但不定义部分应用的功能。 部分函数的允许值范围有限。 例如,如果x = 0 ,则数学函数1/x无效。 局部函数提供了一种定义允许值约束的方法。 在清单4的collect示例中,为Int定义了大小写,但没有为String定义了大小写,因此不收集这seven字符串。

    要定义部分函数,​​还可以使用PartialFunction特性,如清单5所示。

    清单5.在Scala中定义部分函数
    val answerUnits = new PartialFunction[Int, Int] { def apply(d: Int) = 42 / d def isDefinedAt(d: Int) = d != 0 } assert(answerUnits.isDefinedAt(42)) assert(! answerUnits.isDefinedAt(0)) assert(answerUnits(42) == 1) //answerUnits(0) //java.lang.ArithmeticException: / by zero

    在清单5中,我从PartialFunction特性导出了answerUnits ,并提供了两个函数: apply()和isDefinedAt() 。 apply()函数计算值。 我用isDefinedAt() -所需的方法对一个PartialFunction -定义确定参数是否适合约束。

    因为您还可以使用case块实现部分函数, answerUnits 清单5中的 answerUnits可以更简洁地编写,如清单6所示。

    清单6. answerUnits替代定义
    def pAnswerUnits: PartialFunction[Int, Int] = { case d: Int if d != 0 => 42 / d } assert(pAnswerUnits(42) == 1) //pAnswerUnits(0) //scala.MatchError: 0 (of class java.lang.Integer)

    在清单6中,我将用case与保护条件结合使用来约束值并同时提供结果。 与清单5的一个显着区别是MatchError (而不是ArithmeticException )-因为清单6使用了模式匹配。

    局部函数不限于数字类型。 您可以使用所有类型,包括Any 。 考虑实现一个增量器,如清单7所示。

    清单7.在Scala中定义一个增量器
    def inc: PartialFunction[Any, Int] = { case i: Int => i + 1 } assert(inc(41) == 42) //inc("Forty-one") //scala.MatchError: Forty-one (of class java.lang.String) assert(inc.isDefinedAt(41)) assert(! inc.isDefinedAt("Forty-one")) assert(List(42) == (List(41, "cat") collect inc))

    在清单7中,我定义了一个部分函数来接受任何类型的输入( Any ),但选择对类型的子集做出React。 但是,请注意,我也可以为部分函数调用isDefinedAt()函数。 用case的PartialFunction特性的实现者可以调用isDefinedAt() ,它是隐式定义的。 在清单4中 ,我说明了map和collect行为不同。 局部函数的行为解释了差异: collect被设计为接受局部函数并为元素调用isDefinedAt()函数,而忽略那些不匹配的元素。

    Scala中的部分函数和部分应用的函数在名称上相似,但是它们提供了一组不同的正交特征。 例如,没有什么可以阻止您部分应用部分函数。

    在Groovy中

    作为“ 功能性思考,第3部分 ”中的功能性思维系列的一部分,我将详细介绍Groovy中的currying和部分应用程序。 Groovy通过curry()函数实现了currying,该函数源自Closure类。 尽管有这个名字, curry()实际上通过操纵下面的闭包来实现部分应用。 但是,您可以通过使用部分应用程序将一个函数简化为一系列部分应用的单参数函数来模拟currying,如清单8所示。

    清单8. Groovy的部分应用程序和currying
    def volume = { h, w, l -> return h * w * l } def area = volume.curry(1) def lengthPA = volume.curry(1, 1) //partial application def lengthC = volume.curry(1).curry(1) // currying println "The volume of the 2x3x4 rectangular solid is ${volume(2, 3, 4)}" println "The area of the 3x4 rectangle is ${area(3, 4)}" println "The length of the 6 line is ${lengthPA(6)}" println "The length of the 6 line via curried function is ${lengthC(6)}"

    在清单8中,在两种length情况下,我都使用curry()函数部分地应用参数。 但是,使用lengthC ,我会通过部分应用参数直到lengthC单参数函数来创建currying的幻觉。

    在Clojure中

    Clojure包含(partial f a1 a2 ...)函数,该函数采用函数f和数量少于要求的参数,并返回提供剩余参数时可调用的部分应用函数。 清单9显示了两个示例。

    清单9. Clojure的部分应用程序
    (def subtract-from-hundred (partial - 100)) (subtract-from-hundred 10) ; same as (- 100 10) ; 90 (subtract-from-hundred 10 20) ; same as (- 100 10 20) ; 70

    在清单9中,我将subtract-from-hundred定义为部分应用的-运算符(Clojure中的运算符与函数没有区别),并提供100作为部分应用的自变量。 Clojure中的部分应用程序可用于单参数和多参数函数,如清单9中的两个示例所示。

    因为Clojure是动态类型的,并且支持可变参数列表,所以currying并没有实现为语言功能。 部分应用程序处理必要的情况。 然而,命名空间私有(defcurried ...)功能的Clojure添加到减速器库(见相关信息 ),使的该库中的某些功能备受容易定义。 鉴于Clojure Lisp遗产的灵活性,将(defcurried ...)的使用范围扩大到更广泛的范围是微不足道的。

    常见用途

    尽管定义复杂且实现细节繁琐,但在实际编程中,currying和部分应用程序确实占有一席之地。

    功能工厂

    在使用传统的面向对象语言实现工厂功能的地方,咖喱(和部分应用程序)效果很好。 作为示例,清单10在Groovy中实现了一个简单的adder功能。

    清单10. Groovy中的加法器和增量器
    def adder = { x, y -> x + y} def incrementer = adder.curry(1) println "increment 7: ${incrementer(7)}" // 8

    在清单10中,我使用adder()函数派生incrementer函数。 类似地,在清单2中 ,我使用部分应用程序来创建该函数的本地更简洁的版本。

    模板方法设计模式

    四种设计模式之一是“模板方法”模式。 其目的是帮助您定义使用内部抽象方法的算法Shell,以实现以后的实现灵活性。 部分应用和计算可以解决相同的问题。 使用部分应用程序提供已知的行为,而将其他参数留给实现特定细节,可以模仿这种面向对象设计模式的实现。

    隐含值

    与清单2相似,这是一个常见情况,其中您有一系列带有相似参数值的函数调用。 例如,当您与持久性框架进行交互时,必须将数据源作为第一个参数传递。 通过使用部分应用程序,您可以隐式提供值,如清单11所示。

    清单11.使用部分应用程序提供隐式值
    (defn db-connect [data-source query params] ...) (def dbc (partial db-connect "db/some-data-source")) (dbc "select * from %1" "cust")

    在清单11中,我使用了便利dbc函数来访问数据函数,而无需提供数据源,该数据源是自动提供的。 面向对象编程的本质-一个隐含的想法this背景下,看起来好像在每个功能神奇的-可以通过钻营提供实现this的每个功能,使得它无形的消费者。

    结论

    咖喱和部分应用程序以各种形式出现在所有Java.next语言中。 您可以使用这两种技术中的任何一种来进行更简洁的函数定义,提供隐式值并构建函数工厂。

    在下一部分中,我将展示所有Java.next语言的功能编程功能之间令人惊讶的相似之处,以及这些功能有时有时完全不同的实现细节。


    翻译自: https://www.ibm.com/developerworks/java/library/j-jn9/index.html

    Processed: 0.010, SQL: 9