

file:functional-ruby.org
source link: https://blog.oyanglul.us/functional-ruby.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Functional Ruby
Table of Contents

说到 ruby 都会觉得是纯面向对象语言,所有东西都是对象。但是,函数式与面向对象并无冲突(你看看Scala)。最近一个项目用 ruby 写了一个非常常用的 feeder,一不小心写得函数式了些,让我们看看fancy的ruby到底能干些什么fancy的函数式。
lambda
不出所料,函数式一定要先有 lambda,跟所有的 ruby 对象一样,lambda 也就是一个正常的对象
plus1 = ->(x) { x + 1 }
#<Proc:0x007fbaea988030@-:3 (lambda)>
明显,lambda 构造出一个 Proc 的实例,如果我们调用这个 lambda,效果跟 method 没有什么区别:
plus1 = ->(x) { x + 1 }
plus1.(3)
4
好玩的是,method 不能高阶
def plus1 x
x + 1
end
[1,2,3,4].map &plus1
`plus1': wrong number of arguments (0 for 1) (ArgumentError)
因为 plus1 在引用时就已经调用了,解释器在调用 plus1
时发现并没有传参数,于是抛出参数不匹配错误。由于method 引用即invoke,你永远无法写出高阶函数的效果。
而 lambda 就可以:
plus1 = ->(x) { x + 1 }
[1,2,3,4].map &plus1
[2, 3, 4, 5]
神奇的 &
这里的 magic 是 &
把 plus1
变成 Block 发给数组了,Block 也就是我们常见的 {}
,等价于:
[1,2,3,4].map {|x| x + 1}
也等价于:
[1,2,3,4].map &Proc.new{|x| x + 1 }
注意如果没有 &
,解释器无法分辨到底在调用 map
时,把 Proc 当成正常参数,而不是 block
当得知 &
的魔法之后,我们很容易解释 &:symbol
这个语法糖
%w(ouyang jichao).map &:capitalize
["Ouyang", "Jichao"]
desuger 完其实就是
%w(ouyang jichao).map &Proc.new(|x| x.send(:capitalize))
为什么可以产生这样的语法糖,是 Symbol 类型有 to_proc
方法,当 &
尝试将后面的东西变成 Proc 类型后传给 map 当 Block, to_proc
就是用来转换成 proc 的方法。
所以就是:
%w(ouyang jichao).map &:capitalize.to_proc
["Ouyang", "Jichao"]
为什么 lambda 是 proc
话说回来,既然 lambda 也返回 Proc 实例, Proc.new
也返回 Proc 实例,为何要设计这两种匿名函数呢?
简单来说, Proc 只是一段代码块,你可以想象引用的地方会变成这块代码块,而 lambda 不仅是一块代码块,表现得更像一个函数。具体来讲,就是 return 与参数检查:
return
来看个诡异的,下面这段代码我们可能会期望是返回一个数组,只是 jichao
会变成 lulu
而已
%w(ouyang jichao).map { |x| return 'lulu' if x == 'jichao'; x}
"lulu"
显然 return 之后的代码就再也走不到了,整个map会直接返回
但是如果你用 lambda 而不是普通 Proc,你会发现
%w(ouyang jichao).map &->(x){ return 'lulu' if x == 'jichao'; x}
["ouyang", "lulu"]
嗒哒,输出我们的期望了,lambda 的表现跟一个普通函数是一样的,函数的 return 当然不会导致调用者的返回。
确切的说是参数元数 arity 的检查,比如随便定义一个method,如果你给的参数元数不匹配,会得到一个异常
def heheda who
"heheda #{who}"
end
heheda
`heheda': wrong number of arguments (0 for 1) (ArgumentError)
因为定义的是一元的函数,调用时并没有给任何参数,就挂了
但是 Proc 是不会管这个的
heheda = Proc.new{|who| p "heheda #{who}"}
heheda.()
"heheda "
Proc 完全不会理会参数,如果binding能找到,就用了,如果没有,也继续运行。
lambda,则更像一个method
heheda = lambda {|who| p "heheda #{who}"}
heheda.()
`block in main': wrong number of arguments (0 for 1) (ArgumentError)
通常面向对象的捕捉一个绑定通常会通过 @
class HeHe
def initialize who
@who = who
end
def heheda
"heheda #{@who}"
end
end
HeHe
对 who 进行了封装,如果需要访问 who
需要通过 heheda
方法。
同样的东西,在函数式叫闭包,通过闭包我们依然能找到闭包内的绑定
who = 'jichao'
heheda = ->(){ "heheda #{who}" }
def hehedaToOuyang &heheda
who = 'ouyang'
heheda.()
end
hehedaToOuyang &heheda
"heheda jichao"
注意看 heheda 找到的绑定不是离他调用最近的 who
, 而是当初定义的 who=jichao
所以跟面向对象一样, heheda
完美的封装了 who
,调用者即无法直接获取到他绑定的 who
, 也无法重新给他新的绑定
pattern matching
ruby 支持简单的几种模式匹配
destructure
first, *middle_and_last = ['Phillip', 'Jay', 'Fry']
p first, middle_and_last
Phillip | (Jay Fry) |
destructuring 一个数组如此简单,但是hash就不这么容易,好在,方法的参数会自带 destructure的功能:
fry = {first: 'Phillip', middle: 'Jay', last: 'Fry'}
def printFirstName first:, **rest
p first, rest
end
printFirstName fry
Phillip | (:middle=> Jay :last=> Fry) |
这玩意 ruby 叫它 keyword arguments, first:
会匹配 fry
中的 first
并将值绑定到 first
, **rest
绑定剩下的所有东西。
数组也可以这样搞:
1: fry = ['Phillip', 'Jay', 'Fry']
2: def printFirstName first, *rest
3: p first, rest
4: end
5: printFirstName *fry
Phillip | (Jay Fry) |
要注意第5行, 调用时记得给数组加 *
, 这样解释器才知道不是把整个 fry 扔给 printFirstName
当参数,而是把 fry 的内容扔过去当参数。
case when
ruby 中的 case http://docs.ruby-lang.org/en/2.2.0/syntax/control_expressions_rdoc.html#label-case+Expression 可以搞定四种模式匹配
这个很简单,应该都有用过
me = 'ouyang'
case me
when 'ouyang'
"hehe #{me}"
else 'hehe jichao'
end
hehe ouyang
class Me
def initialize name
@name = name
end
def heheda
"heheda #{@name}"
end
end
me = Me.new 'ouyang'
case me
when Me
me.heheda
else
'hehedale'
end
"heheda ouyang"
跟 if else
一样用
require 'ostruct'
me = OpenStruct.new(name: 'jichao', first_name: 'ouyang')
case
when me.name == 'jichao'
"hehe #{me}"
else 'gewuen'
end
hehe #<OpenStruct name="jichao", first_name="ouyang">
lambda (aka guard)
require 'ostruct'
me = OpenStruct.new(name: 'jichao', first_name: 'ouyang')
case me
when ->(who){who.name=='jichao'}
"hehe #{me}"
end
hehe #<OpenStruct name="jichao", first_name="ouyang">
正则
case 'jichao ouyang'
when /ouyang/
"heheda"
end
heheda
其实只是个简单的语法糖
case when 并不是magic,其实只是 if else 的语法糖, 比如上面说的正则
if(/ouyang/ === 'jichao')
"heheda"
end
所以 magic 则是所有 when 的对象都实现了 ===
方法而已
- 值:
object.===
会代理到==
- 类型:
Module.===
会看是否是其 instance - 正则:
regex.===
如果匹配返回 true - 表达式:取决于表达式返回的值的
===
方法 - lambda:
proc.===
会运行 lambda 或者 proc
这样,我们可以随意给任何类加上 ===
方法, 不仅如此,实现一个抽象数据类型(ADT)会变得是分简单
一个简单的例子
一个简单的 feeder 流程大概是,从一个或多个数据源获取数据并 feed 到一个地方(DB, S3, ElasticSearch之类)。通常是一个定期的任务,比如没多久就 feed 那么一次。
作为定期跑的任务,我们需要监控两个方面
- feed 失败了多少
- feeder 跑了没
不管是什么形式,监控都不应该跟我们的业务搞到一起去,比如
一个简单的 Either Monad http://hackage.haskell.org/package/base-4.8.2.0/docs/src/Data.Either.html#Either
创建一个刚好够用的 Either 非常简单
Functor
module Either
def initialize v
@v = v
end
def map
case self
when Right
Right.new(yield @v)
else
self
end
end
alias :fmap :map
Monad
def bind
case self
when Right
yield @v
else
self
end
end
alias :chain :bind
alias :flat_map :bind
一个好看的 inspect
def inspect
case self
when Left
"#<Left value=#{@v}>"
else
"#<Right value=#{@v}>"
end
end
end
联合类型 Left | Right
在实现了 Either 接口之后,我们可以很容易的实现 Left | Right
class Left
include Either
def initialize v=nil
@v=v
end
def == other
case other
when Left
other.left_map { |v| return v == @v }
else
false
end
end
end
class Right
include Either
def == other
case other
when Right
other.map { |v| return v == @v }
else
false
end
end
end
这个Either非常轻量, 我还是把它抽成gem以便单独管理, 与其他一些 Maybe 和 Free 一块收到 cats.rb 中.
用 Either 做控制流
1: def run
2: list_of_error_or_detail =
3: listof_error_or_id.map do |error_or_id| # <-
4: error_or_id.flat_map do |id| # <-
5: error_or_detail_of(id) # <-
6: end
7: end
8: list_of_error_or_detail.map { |error_or_detail| error_or_saved error_or_detail} # <-
9: end
listof_error_or_id
是一个 IO, 去某个地方拿一串 id, 或者返回一串错误, 所以类型是[Either error id]
- 所以
error_or_id
的类型是Either error id
,flat_map
可以把id
取出来, 如果有的话 - 取出来的
id
交给error_or_detail_of
, 该函数也是 IO, 复杂获得对应 id 的 详细信息, 是IO就有可能会有错误, 所以返回值类型也是Either error detail
- 这时, 如果是用
fmap
转换完成后会变成一个Either error (Either error detail)
. 但显然我们不需要嵌套这么多层,flat
一些会变成Either error detail
- 后面的 save 函数也是类似的 IO 操作, 返回
Either error saved
那么我们的业务逻辑的流程走完了,该负责监控的逻辑了,注意现在 run 的返回值类型是 Either[Error, [Either[Error, Data]]]
failures, success = run.partition {|lr| !lr.is_a? Right}
error_msg = failures.map do |failure|
failure.left_map &:message
end.join "\n"
logger.error "processing failure #{failues.length}:\n#{error_msg}" unless error_msg.blank?
logger.info "processing success #{success.length}: #{success}"
actor model 多线程
当你的数据处理都是函数式的之后,或者说 immutable,应用多线程将是十分简单而且安全的事情, 下面也是一个简单的例子,使用 Celluloid 把我们的 feeder 改成多线程
require "celluloid/autostart"
module Enumerable
def pmap(&block)
futures = map { |elem| Celluloid::Future.new(elem, &block) }
futures.map(&:value)
end
end
你懂的,把我们feeder的 map
都换成 pmap
,多线程就这么简单
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK