函数式编程Smalltalk

42次阅读

共计 2448 个字符,预计需要花费 7 分钟才能阅读完成。

这篇文章是为了熟悉, 函数式编程中的消息传递


安装

https://github.com/mcandre/gst-win

下载安装,然后打开 cmd(不能使用 git bash,原因未知),输入 gst 即可开始 Smalltalk 之旅

语法

1. (1 + 2) * 3 // 9
2. 'Hello, world' printNl //'Hello, world' (必须是单引号)
3. Array new: 20 (创建一个数组: 长度 20)
4. x := Array new: 20 (创建长度 20 的数组, 并赋值给 x)!

5. x at: 1 put: 99 (改变 x 的数组第一项, 值为 99)

6. (x at: 1) + 1 (把 x 里面第一项取出来, 加上 1,x 本身不变)

7. x := Set new (创建一个 Set)

8. x add: 5. x add: 7. x add: 'foo' (往 Set 里添加)
  x add: 5; add: 7; add: 'foo' (简写)


9. x remove: 5 (删除其中的一条)

10. x includes: 5 (是否存在这条)

11. y := Dictionary new (创建一个字典)

12. y at: 'One' put: 1 (往 y 的这个字典里面放 1)

之前我一直搞不懂为什么有些人说 person.cut(‘xxx’) 是

  1. 给 persom 对象发送了一个 cut 消息
  2. person 对象会响应这个消息

我们学 js 的时候理解, 这就是一个函数调用, 不懂为什么会这么说.
但是,改成 Smalltalk 就理解了

person cut: 'xxx' (给 person 传递一个消息, 切掉 xxx)
person cut: 'xxx'; cut: 'hands' (给 person 传递多个消息)

面向对象的核心就是对象与对象之间交互。

  1. 对象维护自己的状态和生命周期
  2. 每个对象独立
  3. 对象和对象直接通过 消息传递 来工作

现在想想这个名字还是很有意思的Smalltalk: 说小话, 好像是上课传纸条

面向对象与函数式的关系

请带着如下问题来看下面的文章

  1. 面向对象和函数式是对立的(不可融合的)吗?// 不对立
  2. 两者的优缺点是什么?

通过一个取钱的程序来看上面的问题


// 创建一个 money 变量 和 take 函数(set! set 是设置这个值,! 代表当前可以使用 set)
(define money 100)
(define (take n)
    (set! money (- money n))
    money
)

可以将 money 改为局部变量

(define taker
    (let (money 100)
        (lambda (n)
            (set! money (- money n))
            money)
    )
(taker 25)
75
(taker 25)
50

或者

(define (taker money)
    (lambda (n)
        (set! money (- money n))
        money)
(define taker1 (taker 100))
(define taker2 (taker 100))
(taker1 50)
50
(taker2 70)
30
  1. 同样一个函数,每次执行的结果却不一样。
  2. 闭包可以存状态,不同的函数有各自的状态

函数式也可以做成消息传递风格

使用消息传递风格就可以构造 account 对象了,account 对象可以响应 withdraw(取钱)和 deposit(存钱)消息:

// 函数 money 内部返回的是 dispatch 函数
let makeAccount = money => {let take = (n) => {
    money = money - n
    return money
  }
  let save = (n) => {
    money = money + n
    return money
  }
  let dispatch = (m) => {
    return (
    m === 'take' ? take :
    m === 'save' ? save :
    new Error('unknown request'))
  }
  return dispatch
}

接下来是使用 makeAccount 创造两个 account 对象(其实是过程):

let account1 = makeAccount(100)
account1('take')(70) // 30
account1('save')(50) // 80

写成 Lisp 其实更像是消息传递

((account1 'withdraw) 50)
((account1 'deposit) 50)

以上只是利用『分派』模式来模拟对象,函数式编程有完整的面向对象体系,大家有兴趣可以自行了解。

赋值的利弊

引用原文的一句话:

将赋值引进程序设计语言,将会使我们陷入许多困难概念的丛林中。

但是它就没有好处吗?
好处: 每个对象可以存储自己的状态, 封装的更彻底, 不需要关心数据会怎么变, 因为这个对象会维护这个数据

与所有状态都必须显示地操作和传递额外参数的方式相比,通过引进赋值以及将状态隐藏在局部变量中的技术,能让我们以一种更 ** 模块化 ** 的方式构造系统。

但是这本书马上又加了一句话:

可惜的是,我们很快就会发现,事情并不是这么简单。

赋值的代价

1. 赋值操作使我们可以去模拟带有局部状态的对象,但是,这是有代价的,它是我们的程序设计语言不能再用代换模型来解释了。进一步说,任何具有「漂亮」数学性质的简单模型,都不可能继续适合作为处理对象和赋值的框架了。2. 只要我们不使用赋值,一个「过程 / 函数」接收同样的参数一定会产生同样的结果,因此就可以认为这个「过程 / 函数」是在计算「数学函数」。3. 不用任何赋值的程序设计称为函数式程序设计。

示例

pager 组件的页码生成过程 看代码的时候一定要考虑有没有赋值(一个变量第一次出现等于号是定义, 当被定义的改变叫做赋值)

赋值的本质

1. 如果没有赋值,money 只不过就是一个值的名字;2. 如果有了赋值,money 就是一个容器,可以保存不同的值。3. 这带来的问题很多。广泛采用赋值的程序设计叫做『命令式 / 指令式』程序设计。4. 命令式程序在遇到『并发』『克隆』等问题时经常很令人头疼。5. 举例说明一下。

函数式编程的特点

  1. 数学!(公理化和可证明)
  2. 更加强调程序执行的结果而非执行的过程
  3. 函数是一等公民(函数可以作为参数 – 高阶函数)
  4. 纯函数,拒绝副作用(也就是赋值)
  5. 不可变数据
  6. 数据即代码,代码即数据(本课没有涉及)
  7. 引用透明(本课没有涉及)

正文完
 0