Ruby Ruby 2.5 中的 yield_self

CooperFu · 2018年01月16日 · 最后由 CooperFu 回复于 2018年01月17日 · 2958 次阅读

ruby 2.5 中的 yield_self

这篇文章在 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 的管道有一些类似. 看看他能做什么有意思的事情。

产生自己的 action

下面是一份特别典型的 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

看着好一点么?这个东西仁者见仁智者见智。但是可以列举一些好处:

  1. 至少不用从右往左读,自上而下更符合阅读的顺序。
  2. 代码变成流式,假如需要添加更多步骤,不会影响可读性。

但是也有一些坏处:

  1. 他比原始版本更冗长。使用了很多不必要的块。
  2. 这并不是一个惯用的 ruby 代码。不知道 yield_self 的人不知道为何要这么写。

换一个例子试试看,比如咱们业务中用到的查询某些酒店:

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

p.s.

项目越大越不好升级,看到这些新特性希望能焕发你对 ruby 的爱=-=

Rei 将本帖设为了精华贴。 01月16日 14:31
Rei 取消了精华贴。 01月16日 14:32

希望把原文链接放到前面。

Rei 回复

好哒

这是不是误解了它的作用?

它是为了提高一行解决问题的用完即弃的命令行脚本的编写效率。

写完一段代码后,突然想到它应该作为一个参数,那么你不用 ctrl + A 回到行首插入调用函数再 ctrl + E 到行末补上括号,你只要 yield_self 继续往右写即可,就这样...

luikore 回复

可不可以和你讨论一下遗传算法吗??

Angus 回复

呃,具体的问题是啥?有的话可以另开一帖

强烈建议高亮加上 ruby


# 这样应该是可以用的

"https://api.github.com/repos/rails/rails"
  .yield_self(URI.method(:parse))
  .yield_self(Net::HTTP.method(:get))
  .yield_self(JSON.method(:parse))
  .yield_self { |it| it.fetch("forks_count") }
  .yield_self { |it| "Rails 项目有 #{it} 个 forkers" }
  .yield_self(Kernel.method(:puts))

lithium4010 回复

改好啦!

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