这篇文章在 ruby-weekly10 月的一篇中有提到,翻了翻 ruby-china 并没有发现这个的介绍,翻译加更正一波。
原文链接:https://mlomnicki.com/yield-self-in-ruby-25/
请叫我勤劳的搬运工。
ruby 2.5 加了一个比较有意思的方法 yield_self
来看一下官方定义,稍微简化下是这样的:
class Object
def yield_self
yield(self)
end
end
看起来不是一个特别明显的 feature, 他只是返回了 block 中返回的内容。但是如果你对 Elixir 有了解的话,这和 Elixir 的管道有一些类似. 看看他能做什么有意思的事情。
下面是一份特别典型的 ruby 代码,读取 data.csv 解析一列求和。
CSV.parse(File.read(File.expand_path("data.csv"), __dir__))
.map { |row| row[1].to_i }
.sum
这样的代码通常需要几秒钟才能理解。主要是因为我们正常阅读顺序是从左到右读,但是反应过来需要从右往左读。
咱们来试试用 yield_self
重写
"data.csv"
.yield_self { |name| File.expand_path(name, __dir__) }
.yield_self { |path| File.read(path) }
.yield_self { |body| CSV.parse(body) }
.map { |row| row[1].to_i }
.sum
看着好一点么?这个东西仁者见仁智者见智。但是可以列举一些好处:
但是也有一些坏处:
换一个例子试试看,比如咱们业务中用到的查询某些酒店:
scope = Pois::Hotel.available
scope = scope.where(place_id: params[:place_id]) if params[:place_id]
scope = scope.limit(params[:limit]) if params[:limit]
...
scope
如果换成 yield_self
的方式:
Pois::Hotel.available
.yield_self { |scope| params[:place_id] ? scope.where(place_id: place_id) : scope }
.yield_self { |scope| params[:limit] ? scope.limit(params[:limit]) : scope }
同理,yield_self 的代码变的更冗长,另一方面 我们不用重写scope
这个变量,并不用在最后一行显式的返回他。
看起来仅仅影响了命名?往下看。
再看一个例子:
"https://api.github.com/repos/rails/rails"
.yield_self { |url| URI.parse(url) }
.yield_self { |url| Net::HTTP.get(url) }
.yield_self { |response| JSON.parse(response) }
.yield_self { |repo| repo.fetch("forks_count") }
.yield_self { |forkers| "Rails 项目有 #{forkers} 个forkers" }
.yield_self { |string| puts string }
看起来并不是很好,用了太多不必要的块来做某些很简单的事。但是你看如果我改成:
"https://api.github.com/repos/rails/rails"
.yield_self { |it| URI.parse(it) }
.yield_self { |it| Net::HTTP.get(it) }
.yield_self { |it| JSON.parse(it) }
.yield_self { |it| it.fetch("forks_count") }
.yield_self { |it| "Rails 项目有 #{it} forkers" }
.yield_self { |it| puts it }
我的天呐 想一想 我们真的需要给那么多变量命名吗?改成 it
好像更容易理解了?
为了方便你理解,我写成正常的方式大家看一下:
uri = URI.parse("https://api.github.com/repos/rails/rails")
response = Net::HTTP.get(uri)
repo = JSON.parse(response)
puts "Rails 项目有 #{repo.fetch("stargazers_count")} forkers"
你会发现,我这个方法只是想看一下 rails 项目有多少人 fork 过,却用了 3 个临时变量支撑这个返回值,只是为了更易懂。
咱们项目中 所有 URI.parse()
的返回值,永远是uri
; 所有 http 请求的返回值永远都是 response
; 诸如此类还有很多。
如果用yield_self
. 我们就不用命名这种临时变量了。
希望之后 ruby 可以写成这样
"https://api.github.com/repos/rails/rails"
.yield_self(URI->parse)
.yield_self(Net::HTTP->get)
.yield_self(JSON->parse)
.yield_self { |it| it.fetch("forks_count") }
.yield_self { |it| "Rails 项目有 #{it} 个 forkers" }
.yield_self(Kernel->puts)
哈哈哈只是展望 上面的代码并不会工作 /(ㄒo ㄒ)/~~
Object#yield_self
可以理解成,把数据从一个块中传递到另一个块的管道。稍微遗憾的是他没有一个简短的名字,比如 pipe
或者 apply
. 当然 rails 可以实现这种小魔法。
比如我们项目中坑了某个小伙伴一整天的:
module ActiveRecord
class Base
class << self
alias batch_import import
remove_method :import
end
end
end
项目越大越不好升级,看到这些新特性希望能焕发你对 ruby 的爱=-=