Ruby Ruby 中的函数式编程

quakewang · 发布于 2015年04月30日 · 最后由 chiangdi 回复于 2015年05月11日 · 4434 次阅读
162
本帖已被设为精华帖!

为培训由其他编程语言转到Ruby的程序员,写了一些PPT,其中涉及到了一些函数式编程,反馈还不错,于是抽取出来写了一篇短文。

什么是函数式编程

跳过...请自己Google

函数式编程有什么好处

会让你的代码看起来更屌(实际并不... 会让你的工资增加(做梦... 会让你写的程序性能更好(一般来说没差别...

到底有什么好处...请自己Google

请先做道题

奇偶归一猜想(英语:Collatz conjecture),是指对于每一个正整数,如果它是奇数,则对它乘3再加1,如果它是偶数,则对它除以2,如此循环,最终都能够得到1。 如n = 6,根据上述数式,得出序列6, 3, 10, 5, 16, 8, 4, 2, 1。(步骤中最高的数是16,共有8个步骤) 如n = 8,根据上述数式,得出序列8, 4, 2, 1。(步骤中最高的数是8,共有3个步骤) 如n = 11,根据上述数式,得出序列11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1。(步骤中最高的数是52,共有14个步骤)

给定一个数组,对这个数组里面的每个数字做奇偶归一操作,找出步骤最多的一个序列,比如输入[4, 6, 8, 11] 返回[11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1]

Ruby中的函数式编程

不更新变量

array的 << vs +

def f(array)
  array << 42
end

def f(array)
  array + [42]
end

hash的 []= vs merge

def f(hash)
  hash[:foo] = 42
  hash
end

def f(hash)
  hash.merge(foo: 42)
end

不依赖外部变量

一个例子,常见其他语言转过来的程序员写的ruby代码,将一个数组中的字符串转换成大写输出:

countries = []
["china", "usa"].each do |name|
  countries << name.upcase
end
countries # => ["CHINA", "USA"]

依赖了外部变量(countries),同时还对变量做了更新(<<)

函数式编程,使用map:

countries = ["china", "usa"].map do |name|
  name.upcase
end # => ["CHINA", "USA"]

再一个例子,加总数组中字符串长度:

length = 0
["china", "usa"].each do |name|
  length += name.length
end
length # => 8

函数式编程,使用inject:

length = ["china", "usa"].inject(0) do |memo, name|
  memo + name.length
end # => 8

还可以使用语法糖,简写方式:

length = ["china", "usa"].map(&:length).inject(0, :+)

了解Enumerable/Array/Hash的各种方法:map/inject/select/reject/scan/each/each_pair/each_cons...会让你写出不一样的代码。

Currying

另外一个比较好玩的特性,举个例子,我们可以假装发明了操作符前置的中文编程:

计算 = -> method, a, b { a.send method, b }
 = 计算.curry[:+]
 = 计算.curry[:-]
 = 计算.curry[:*]
 = 计算.curry[:/]
[2, [4, 10]] # => 42

1 = .curry[1]
1[41]         # => 42

在Ruby里面proc调用可以用proc.call, proc.(), 或者proc[], 我比较喜欢proc[]这种方式。

回到最初的题目

首先写一个函数,来做奇偶判断对应的操作:

f0 = -> x {x.even? ? x / 2 : x * 3 + 1}

再写个函数,来判断是否到1,返回序列:

f1 = -> x {x == 1 ? [x] : [x] + f1[f0[x]]}

最后写个函数,将输入的数组做map,然后取序列最大的:

f2 = -> x {x.map{|e| f1[e]}.max_by{|e| e.size}}

输出:

f2[(10..20).to_a]
 => [18, 9, 28, 14, 7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1] 

你最初写的代码会是怎样?对比一下,是否函数式编程真的体现了 ”describe what to do, rather than how to do it“

共收到 19 条回复
2107

讲得很屌

De6df3

代码可以着色

4215

变量可以中文啊,第一次看到。高

12224

:plus1: 更喜欢 ->(a){ a*a }.(2) lambda 表达式些 😄

18464

好叼呀

96

不错哇

5696

看起来是很屌,如果遇到性能要求高的需求,还是不太适用

9楼 已删除
9572

@Grant 为啥?

5696

@crosspass 不能更新变量,创建新变量必定会产生额外的开销影响性能。

17945

只能说看上去很威武霸气...

8134

优雅

6224

计算机最终是how do it的,这个架构下还是怎么让how do it描述的轻松些比较重要。

12381

居然看懂了

1
[4, 6, 8, 11].map { |n|
  array = [n]
  until n == 1
    n = n.odd? ? (n * 3 + 1) : (n / 2)
    array << n
  end
  array
}.max_by(&:length)

用了递归之后更抽象更难理解,让人感觉自己更聪明,而局部变量能帮助人理解过程。长远维护来看我还是避免递归。

函数式语言有一些好的特性我觉得可以借鉴,例如变量不可更新 -> 用于并行计算;模式匹配,用于文档解析。我分不太清什么是函数式编程,有用的特性就借鉴。

17楼 已删除
4215

#16楼 @rei 我觉得三个函数的方式好些,因为很容易测试每个函数,容易查错。 你的方式,代码互相耦合,容易互相影响。 [] << 这种大部分能用map来实现。我也正在改习惯,多用map。

1

#17楼 @deathking 你用这么多字解释递归更好理解不觉得有问题么?

96

楼主的例子,包括回帖里的解释递归并不是在说函数式编程。 你们只是在解释一个函数的用法,以及一些特殊的机制,对于这两者,是几乎每种语言都可以做到的,并非ruby的优点。 而函数式编程,就如面向对象编程一样,是个“伪概念”,但又确实的影响着编程模式,就如C可以作面向对象编程,但语言标准不方便于该编程模式一样甚至更甚之,ruby的语言标准既不方便于函数式编程,其机制也不利于函数式编程。 所谓语言标准不方便,函数式编程的一个基本行为模式叫做"依赖注入",即函数作为值传递,其他语言完全可以通过位置决定其性质,如python,lisp-1,而ruby不行,ruby必须专门通过lambda声明,且通过.call来决定性质,这比其他多范型的或直接自称为函数式的语言要麻烦的多。而ruby的c实现也决定了ruby不适合函数式编程,其函数的深度与底层运算复杂度的正相关不呈线性,复杂度的增长远迅于深度增长。 ruby的性能调优中,一大项就是对函数深度的浅化。老老实实的做面向对象式的编程。玩具应有玩具的觉悟。

10316

#17楼 @deathking :plus1: 专业

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册