Ruby Ruby 中的函数式编程

quakewang · 2015年04月30日 · 最后由 chiangdi 回复于 2015年05月11日 · 8493 次阅读
本帖已被管理员设置为精华贴

为培训由其他编程语言转到 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“

讲得很屌

代码可以着色

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

匿名 #5 2015年04月30日

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

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

9 楼 已删除

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

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

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

居然看懂了

[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 楼 已删除

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

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

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

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