`
xjk2131650
  • 浏览: 57145 次
  • 性别: Icon_minigender_1
  • 来自: 河北
社区版块
存档分类
最新评论

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

阅读更多

函数世界中的一些经验主义者认为设计模式的概念有缺陷,在函数式编程中不需要。在模式 的狭义解释下该观点可能成立,但这是一个更多关于语义而非使用的论点。设计模式的概念(针对常见问题的指定编目解决方案)是合理的。但是,模式有时在不同的范式下以不同的形式出现。因为构建块和问题解决方法在函数世界中是不同的,一些传统的 Gang of Four 模式(参阅 参考资料)消失了,而其他模式存在问题,但解决问题的方式大相径庭。本期和下一期将研究一些传统的设计模式,并以函数式思维从全新角度来思考它们。

在函数编程领域,传统设计模式通常以三种方式之一表现:

  • 模式由语言吸收。
  • 模式解决方案仍然存在于函数范式中,但是实现细节有所不同。
  • 解决方案使用其他语言或范式缺乏的功能实现。(例如,许多使用元编程的解决方案简洁且优雅,但无法通过 Java 实现。)

我会依次研究这三种情况,在本期中从一些熟悉的模式入手,大部分模式全部或部分地纳入现代语言。

工厂和局部套用(currying)

局部套用 (Currying) 是许多函数语言的一种特性。它是以数学家 Haskell Curry 的名字命名的(Haskell 编程语言也是以该数学家命名),能够对多参数函数进行转换,以便将它用作一串单参数函数进行调用。与此密切相关的是部分应用 (partial application),该技术可以将固定值分配给函数的一个或多个参数,从而生成另一个更小的元数 (arity) 函数(元数是函数参数的个数)。我在 “函数式思维:运用函数式思维,第 3 部分” 中讨论过这两种技术。

在设计模式上下文中,局部套用充当一个函数工厂。函数式编程语言中的一个常见特性是一等(first-class)(或高阶)函数,它允许函数充当任何其他数据结构。多亏这一特性,我可以轻松创建基于一些条件返回其他函数的函数,这就是工厂的精髓。例如,如果您有一个将两个数字相加的通用函数,您可以将局部套用用作一个工厂来创建总是将其参数加 1 的函数,即一个增量器,如清单 1 所示,使用 Groovy 语言实现:


清单 1. 局部套用作为函数工厂
				
def adder = { x, y -> return x + y }
def incrementer = adder.curry(1)

println "increment 7: ${incrementer(7)}" // prints "increment 7: 8"

在 清单 1 中,我将第一个参数局部套用为 1,返回一个接受单一参数的函数。本质上,我创建了一个函数工厂。

当您的语言本机支持这种行为时,它往往被用作其他大小对象的构建块。例如,看看如清单 2 所示的 Scala 示例:


清单 2. Scala 对局部套用的 “随意” 使用
				
object CurryTest extends Application {

  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 dividesBy(n: Int)(x: Int) = ((x % n) == 0)

  val nums = List(1, 2, 3, 4, 5, 6, 7, 8)
  println(filter(nums, dividesBy(2)))
  println(filter(nums, dividesBy(3)))
}

清单 2 中的代码是 Scala 文档中递归和局部套用的示例之一(参阅 参考资料)。filter() 方法通过 p 参数以递归的方式过滤一个整数列表。p 是一个谓词函数,函数领域中用于布尔函数的一个常见术语。filter() 方法检查看列表是否为空,如果为空,就直接返回;否则它通过谓词检查列表中的第一个元素(xs.head),以确定是否应将其包含在过滤的列表中。如果它通过谓词测试,返回的就是一个新列表,其头在前面,过滤的尾部作为剩余部分。如果第一个元素没有通过谓词测试,返回的就只是列表的已过滤剩余部分。

从模式角度来看 清单 2 中比较有趣的是在 dividesBy() 方法中对局部套用的 “随意” 使用。注意,dividesBy() 接受两个参数,并根据第二个参数是否均衡地分为第一个参数,返回 true 或 false。但是,当该方法被作为 filter() 方法调用的一部分被调用时,它只在具有一个参数的情况下被调用,调用结果是一个局部套用过的函数,然后该函数被用作 filter() 方法中的谓词。

本例展示模式在函数式编程中表现的前两种方式,我在本文开始列出过它们。首先,局部套用被构建到语言或运行时中,因此函数工厂的概念是生来就有的,且不需要额外的结构。其次,它展示了我对不同实现的观点。如 清单 2 那样使用局部套用可能从来不会在传统的 Java 编程员身上发生;我们从未真正有过可移植代码,当然也从未想过从更通用的函数构建特定函数。事实上,在这里大部分的开发人员不会想到使用一个设计模式,因为从一个更通用的方法创建一个特定的 dividesBy() 方法似乎是一个小问题,而设计模式(很大程度上依赖于结构来解决问题,因而需要大量开销来实现)似乎是一个针对大问题的解决方案。按照本来意图使用局部套用不会证明一个特殊名称的程序的合理性,除了它已经拥有的名称。

一等函数(First-class)和设计模式

一等函数大大简化了许多常用的设计模式。(命令设计模式甚至消失了,因为您不再需要一个针对可移植功能的对象包装器。)

模板方法

一等函数使模板方法设计模式(参阅 参考资料)更易于实现,因为它们能够移除可能不需要的结构。模板方法定义一个方法中算法的框架,把一些步骤委托给子类,并强制他们在不更改算法结构的情况下定义这些步骤。模板方法的典型实现如清单 3 所示,使用 Groovy 实现:


清单 3. 模板方法的 “标准” 实现
				
abstract class Customer {
  def plan
    
  def Customer() {
    plan = []
  }
    
  def abstract checkCredit()
  def abstract checkInventory()
  def abstract ship()
    
  def process() {
    checkCredit()
    checkInventory()
    ship()
  }
}

在 清单 3 中,process() 方法依赖于 checkCredit()checkInventory() 和 ship() 方法,其定义必须由子类提供,因为它们是抽象方法。

由于一等函数可充当任何其他数据结构,我可以使用代码块重新定义 清单 3 中的示例,如清单 4 所示:


清单 4. 具有一等函数的模板方法
				
class CustomerBlocks {
  def plan, checkCredit, checkInventory, ship
    
  def CustomerBlocks() {
    plan = []
  }
    
  def process() {
    checkCredit()
    checkInventory()
    ship()
  }
}

class UsCustomerBlocks extends CustomerBlocks{
  def UsCustomerBlocks() {
    checkCredit = { plan.add "checking US customer credit" }
    checkInventory = { plan.add "checking US warehouses" }
    ship = { plan.add "Shipping to US address" }
  }
}

在 清单 4 中,算法中的步骤只是类的属性,像任何其他属性一样是可分配的。在这个示例中,语言特性主要地吸收实现细节。将这一模式看作一个问题的解决方案(把步骤委派给后续的处理程序)仍然很有用,不过实现起来比较简单。

两种解决方案不是等同的。在 清单 3 中的 “传统” 模板方法示例中,抽象类需要子类来实现依赖的方法。当然,子类可能仅创建一个空的方法体,不过抽象方法定义形成一种文档,提醒 subclasser 将其考虑在内。另一方面,死板的方法声明可能不适合于需要更多灵活性的情景中。例如,我可以创建我的 Customer 类的一个版本,该类接受任何方法列表以供进行处理。

对代码块等功能的深度支持使语言更具有开发人员友好性。考虑这样一种情况,比如您想让 subclasser 跳过一些步骤。Groovy 有一种特殊的受保护访问运算符 (?.),该运算符确保在调用一个对象的方法前该对象不为空。考虑清单 5 中的 process() 定义:


清单 5. 添加对代码块调用的保护
				
def process() {
  checkCredit?.call()
  checkInventory?.call()        
  ship?.call()
}

在 清单 5 中,实现子类的任何人可以选择要将代码分配哪些子方法,保留其他方法为空。

策略

一等函数简化的另一种流行设计模式是策略模式。策略定义一系列算法,封装每一种算法并使它们能够进行互换。它允许算法随使用它的客户不同而有所不同。一等函数使得构建和操作策略更简单。

用于计算产品数目的策略设计模式的一种传统实现如清单 6 所示:


清单 6. 为具有两个数目的产品使用策略设计模式
				
interface Calc {
  def product(n, m)
}

class CalcMult implements Calc {
  def product(n, m) { n * m }
}

class CalcAdds implements Calc {

  def product(n, m) {
    def result = 0
    n.times {
      result += m
    }
    result
  }
}

在 清单 6 中,我为具有两个数目的产品定义了一个接口。我使用两个不同的具体类(策略)实现接口:一个使用乘法,另一个使用加法。为测试这些策略,我创建了一个测试用例,如清单 7 所示:


清单 7. 测试产品策略
				
class StrategyTest {
  def listOfStrategies = [new CalcMult(), new CalcAdds()]

  @Test
  public void product_verifier() {
    listOfStrategies.each { s ->
      assertEquals(10, s.product(5, 2))
    }
  }
}

如 清单 7 所示,两个策略都返回同一个值。将代码块用作一等函数,我可以降低上一个示例的复杂性。考虑乘方策略用例,如清单 8 所示:


清单 8. 以更低的复杂性测试乘方
				
@Test
public void exp_verifier() {
  def listOfExp = [
      {i, j -> Math.pow(i, j)},
      {i, j ->
        def result = i
        (j-1).times { result *= i }
        result
      }]

  listOfExp.each { e ->
    assertEquals(32, e(2, 5))
    assertEquals(100, e(10, 2))
    assertEquals(1000, e(10, 3))
  }
}

在 清单 8 中,我使用 Groovy 代码块直接定义了两个内联乘方策略。如 模板方法示例 中所示,我以简化繁。传统的方法强制围绕每个策略使用名称和结构,有时这是我们需要的。但是,注意,我建议对 清单 8 中的代码加入更严格的保护措施,鉴于我无法轻松绕过更传统的方法施加的限制,传统方法是一种动态与静态对比参数,而非函数式编程与设计模式对比的参数。

受一等函数影响的模式主要是语言吸收的模式示例。接下来,我要展示保持语义但又更改实现的一种模式。

享元 (Flyweight) 和内存化 (memoization)

享元模式是一种使用共享来支持大量细粒度对象引用的优化技术。您要保持对象池可用,为特定视图创建到该池的引用。享元使用一种规范对象(canonical object) 的思想,即一种表示该类型中所有其他对象的代表性对象。例如,您有一个特定的消费产品,产品的规范版本表示该类型的所有产品。在一个应用程序中,不要为每个用户创建一个产品列表,而要创建一个规范产品列表,每个用户拥有对他们产品列表的引用。

考虑清单 9 中建模计算机类型的类:


清单 9. 建模计算机类型的简单类
				
class Computer {
  def type
  def cpu
  def memory
  def hardDrive
  def cd
}

class Desktop extends Computer {
  def driveBays
  def fanWattage
  def videoCard
}

class Laptop extends Computer 
  def usbPorts
  def dockingBay
}

class AssignedComputer {
  def computerType
  def userId

  public AssignedComputer(computerType, userId) {
    this.computerType = computerType
    this.userId = userId
  }
}

在这些类中,比方说为每个用户创建一个新的 Computer 实例是效率低下的,假定所有计算机具有相同的规格。一个AssignedComputer 会将一种计算机与一个用户关联起来。

让该代码更高效的一种常见方式是将工厂和享元模式相结合起来。考虑生成规范计算机类型的单例工厂,如清单 10 所示:


清单 10. 享元计算机实例的单例工厂
				
class ComputerFactory {
  def types = [:]
  static def instance;
  
  private ComputerFactory() {
    def laptop = new Laptop()
    def tower = new Desktop()
    types.put("MacBookPro6_2", laptop)
    types.put("SunTower",  tower)
  }

  static def getInstance() {
    if (instance == null)
      instance = new ComputerFactory()
    instance
  }

  def ofType(computer) {
    types[computer]
  }  
}

ComputerFactory 类构建可能的计算机类型缓存,然后通过其 ofType() 方法交付适当的实例。这是一种传统的单例工厂,因为您使用 Java 编写它。

但是,单例也是一种设计模式(参阅 参考资料),它是运行时吸收模式的另一个好示例。考虑简化的 ComputerFactory,其中使用 Groovy 提供的 @Singleton 注释,如清单 11 所示:


清单 11. 简化的单例工厂
				
@Singleton class ComputerFactory {
  def types = [:]
  
  private ComputerFactory() {
    def laptop = new Laptop()
    def tower = new Desktop()
    types.put("MacBookPro6_2", laptop)
    types.put("SunTower",  tower)
  }

  def ofType(computer) {
    types[computer]
  }
}

为测试工厂返回规范实例,我编写了一个单元测试,如清单 12 所示:


清单 12. 测试规范类型
				
@Test
public void flyweight_computers() {
  def bob = new AssignedComputer(ComputerFactory.instance.ofType("MacBookPro6_2"), "Bob")
  def steve = new AssignedComputer(ComputerFactory.instance.ofType("MacBookPro6_2"), 
  "Steve") assertTrue(bob.computerType == steve.computerType)
}

跨实例保存常见信息是一个不错想法,这是我在涉足函数式编程时想保留的想法。但是,实现细节相当不同。这是在更改(更合适的说说是简化)实现时保留模式语义 的一个示例。

在 函数式思维:Groovy 中的函数式特性,第 3 部分 中,我介绍了内存化 特性,它能够构建到编程语言中,支持自动缓存递归的函数返回值。换言之,一个内存化函数支持运行时为您缓存值。Groovy 的最新版本支持内存化(参阅 参考资料)。考虑清单 13 中定义的函数:


清单 13. 享元的内存化
				
def computerOf = {type ->
  def of = [MacBookPro6_2: new Laptop(), SunTower: new Desktop()]
  return of[type]
}

def computerOfType = computerOf.memoize()

在 清单 13 中,规范类型在 computerOf 函数内定义。为了创建一个函数的内存化实例,我直接调用 Groovy 运行时定义的 memoize()方法。

清单 14 显示对比两种方法调用的一个单元测试:


清单 14. 对比方法
				
@Test
public void flyweight_computers() {
  def bob = new AssignedComputer(ComputerFactory.instance.ofType("MacBookPro6_2"), "Bob")
  def steve = new AssignedComputer(ComputerFactory.instance.ofType("MacBookPro6_2"), 
  "Steve") assertTrue bob.computerType == steve.computerType

  def sally = new AssignedComputer(computerOfType("MacBookPro6_2"), "Sally")
  def betty = new AssignedComputer(computerOfType("MacBookPro6_2"), "Betty")
  assertTrue sally.computerType == betty.computerType
} 

最终结果是一样的,但注意实现细节却有着巨大差别。对于 “传统” 设计模式,我创建了一个新类来充当工厂,实现两个模式。对于函数版本,我实现了一个方法,然后返回了一个内存化版本。卸载缓存等细节到运行时意味着手写的实现不太可能失败。在本用例中,我保留了享元模式的语义,但具有一个非常简单的实现。

结束语

在本期中,我介绍了设计模式的语义在函数式编程中表现的三种方式。首先,它们可以被语言或运行时吸收。我使用工厂、策略、单例和模板方法模式展示了相关示例。其次,模式可保留其语义,但具有完全不同的实现;我展示了使用类和使用内存化的享元模式示例。第三,函数语言和运行时可以有完全不同的特性,从而支持它们以完全不同的方式解决问题。

在下一期,我将继续研究设计模式和函数式编程的交叉,并展示第三种方法的示例。

分享到:
评论

相关推荐

    Scala函数式编程

    很大篇幅都放在,使用scala实现scala默认库文件的API中,通过对简单的函数式编程逻辑的介绍和实践,主要是实践,建立起来一个比较明晰的scala思维模式,或者叫函数式编程的思维模式。 2 无副作用的函数式编程,同时...

    函数式Swift.epub

    书中还可能涵盖了其他函数式编程概念,如柯里化(Currying)、高阶函数(Higher-Order Functions)、尾递归(Tail Recursion)优化、闭包(Closures)的使用,以及如何通过函数式编程风格来实现常见的设计模式。...

    js函数式编程

    通过以上对函数式编程的基本概念及其在JavaScript中的应用的探讨,我们可以看到函数式编程不仅是一种编程范式,更是一种思维方式。它鼓励开发者以更加抽象和通用的方式来思考问题,从而编写出更加优雅和高效的代码。

    23种设计模式思维导图.zip

    设计模式是软件工程中的一种最佳实践,用于解决在软件开发过程中...设计模式的应用不仅限于面向对象编程,也可以应用于函数式编程和其他编程范式。在实际项目中,合理地运用设计模式可以显著提升软件的质量和开发效率。

    函数式编程初探共2页.pdf.zip

    10. **模式匹配**:许多函数式语言支持模式匹配,这是一种强大的语法构造,能根据输入的不同形式执行不同的操作,简化了条件分支的编写。 由于压缩包内子文件名为"赚钱项目",这可能暗示文档中可能会涉及如何利用...

    函数式编程:Java SE平台上的函数式编程的完整介绍

    5. **函数式设计模式**:例如,使用函数式编程可以实现命令模式(使用函数对象代替具体命令)、策略模式(函数接口作为策略)和装饰器模式(通过函数组合实现动态装饰)。 总之,Java SE平台上的函数式编程提供了新...

    DTCC2014:数据库设计模式变迁.pdf

    7. 集合思维与函数式编程:在数据库操作中,集合思维(Set)是关系型数据库的一个重要概念。例如,通过编写函数将字符串分割成表中的值,利用表值函数来处理集合数据。这种方式可以避免使用光标进行逐条处理,提高...

    函数式编程报告template1

    1. **Lisp** - 由John McCarthy于1958年提出,Lisp是最古老的函数式语言之一,以其括号表示法和强大的元编程能力著称。它的动态类型和递归特性使其成为人工智能研究的重要工具。 2. **Haskell** - 作为纯函数式语言...

    CS1807-U201814745-朱槐志函数式编程1

    Erlang是一种为分布式、并发系统设计的函数式语言。它具有内置的进程间通信机制和容错能力,广泛应用于电信和互联网领域。Erlang的错误恢复机制使得系统更加健壮。 5. Haskell Haskell是最为知名的纯函数式编程语言...

    二十三种设计模式【PDF版】

    1.设计模式更抽象,J2EE 是具体的产品代码,我们可以接触到,而设计模式在对每个应用时才会产生具体代码。 2.设计模式是比 J2EE 等框架软件更小的体系结构,J2EE 中许多具体程序都是应用设计模式来完成的,当你深入...

    设计模式入门之一:深入单例模式

    学习设计模式可以提升我们的设计思维,帮助我们更好地理解和评估现有设计。通过复用这些模式,我们可以快速解决问题,提高团队之间的沟通效率。 单例模式属于创建型设计模式,还有其他如抽象工厂、工厂方法、建造者...

    设计模式全部文件打包

    设计模式是软件工程中的一种重要思想,它代表了在特定情境下解决问题的优秀方案,能够提升代码的可读性、可维护性和复用性。在这个“设计模式全部文件打包”中,我们可以期待找到一系列关于设计模式的学习资料,帮助...

    系统架构设计师思维导图1

    ### 系统架构设计师思维导图1 #### 知识点详析 ##### 一、计算机基础知识 **冯诺依曼系统:** - **特点:** 存储程序原理,程序和数据统一存储在内存中。 - **组成部分:** 输入设备、输出设备、运算器、控制器、...

    行业文档-设计装置-初中函数教学用教具.zip

    此教具包通过多元化的教学手段,旨在打破传统的教学模式,使函数学习不再枯燥,增强学生的实践操作能力和数学思维。教师可以依据学生的学习水平和兴趣选择合适的教具部分进行教学,确保函数知识的有效传授。同时,这...

    ML程序设计教程(原书第二版)中英文对照答案

    "ML程序设计教程"是针对函数式编程语言ML的一本经典教材,由柯伟翻译的第二版,旨在帮助读者深入理解ML语言及其编程思想。ML是函数式编程家族的一员,它的语法简洁,强调数学逻辑和纯函数,这使得它在理论研究和实际...

    设计模式中文版part2

    设计模式是软件工程中的一种最佳实践,用于解决在开发复杂应用程序时经常遇到的常见问题。它们代表了在特定上下文中经过验证的解决方案,可以作为通用的、可重用的蓝图来指导开发人员如何设计和实现代码。在这个...

    从Observer到Observable:使用Functional Swift提升复杂iOS项目的可维护性.pdf

    通过将Observer模式转为Observable模式,利用Swift语言的特性,以及运用函数式编程中的设计模式,开发者能够构建出更加健壮、易于理解和维护的应用程序。这种转变不仅仅是在技术层面的改进,更是思维方式的转变,...

    swift函数编程

    - **目的**:本书旨在教会读者如何采用函数式编程思维方式来进行Swift编程。 - **背景**:虽然苹果官方提供了大量文档,市场上也有诸多Swift相关的书籍,但这本书的独特之处在于强调函数式编程的方法论。 #### 二、...

    导数在研究函数中的应用复习课教学设计说明.doc

    【导数在研究函数中的应用复习...本教学设计关注每个学生的学习进度,针对不同层次的学生设置不同难度的任务,旨在通过合作、自主和探究式学习,使学生全面掌握导数在研究函数中的应用,提高他们的解题技巧和数学素养。

Global site tag (gtag.js) - Google Analytics