Ruby集合/可枚举对象的炫酷技巧和表达性代码片段

5

你最喜欢的ruby集合代码片段是什么?最好是你自己发现的,表达清晰易懂,并能在编程实践中增加一些乐趣。


数组中的模式匹配(用于局部变量和参数):

(a, b), c = [[:a, :b], :c]
[a,b,c]
=> [:a, :b, :c]

(a,), = [[:a]]
a
=> :a

将非数组值赋给多个变量:

abc, a, b =* "abc".match(/(a)(b)./)
=> ["abc", "a", "b"]

nil1, =* "abc".match(/xyz/)
=> []

使用相同的表达式初始化数组元素:

5.times.map { 1 }    
=> [1,1,1,1]

Array.new(5) { 1 }
=> [1,1,1,1,1]

使用相同的值初始化数组:

[2]*5
=>[2,2,2,2,2]

Array.new 5, 2
=>[2,2,2,2,2]

计算数组元素的总和:

[1,2,3].reduce(0, &:+)

=> 6

查找所有符合条件的索引:

a.each_with_index.find_all { |e, i| some_predicate(e) }.map(&:last)

替代CSS类:

(1..4).zip(%w[cls1 cls2].cycle)

=> [[1, "cls1"], [2, "cls2"], [3, "cls1"], [4, "cls2"]]

解压缩:

keys, values = {a: 1, b: 2}.to_a.transpose
keys
=> [:a, :b]

探究字符串的布尔成员方法:

"".methods.sort.grep(/\?/)

探索字符串特定的方法:

"".methods.sort - [].methods
6个回答

2

以下是使用记忆化技术生成的“惰性”斐波那契数列,取自Neeraj Singh

fibs = { 0 => 0, 1 => 1 }.tap do |fibs|
  fibs.default_proc = ->(fibs, n) { fibs[n] = fibs[n-1] + fibs[n-2] }
end

fibs.take(10).map(&:last).each(&method(:puts))

计数排序的一种实现:

module Enumerable
  def counting_sort(k)
    reduce(Array.new(k+1, 0)) {|counting, n| counting.tap { counting[n] += 1 }}.
    map.with_index {|count, n| [n] * count }.flatten
  end
end

实现求和(prefix sum)算法的代码如下:
module Enumerable
  def scan(initial=nil, sym=nil, &block)
    args = if initial then [initial] else [] end
    unless block_given?
      args, sym, initial = [], initial, first unless sym
      block = ->(acc, el) { acc.send(sym, el) }
    end
    [initial || first].tap {|res| 
      reduce(*args) {|acc, el| 
        block.(acc, el).tap {|e|
          res << e
        }
      }
    }
  end
end

在这里,我尝试让Hash#each产生KeyValuePair而不是两个元素的Array。令人惊讶的是,在进行如此残酷的猴子补丁之后,有多少代码仍然可以正常工作。耶,鸭子类型!

class Hash
  KeyValuePair = Struct.new(:key, :value) do
    def to_ary
      return key, value
    end
  end

  old_each = instance_method(:each)
  define_method(:each) do |&blk|
    old_each.bind(self).() do |k, v|
      blk.(KeyValuePair.new(k, v))
    end
  end
end

我一直在尝试的事情是让 Enumerable#=== 执行递归结构模式匹配。我不知道这是否有任何用处。我甚至不知道它是否实际可行。

module Enumerable
  def ===(other)
    all? {|el| 
      next true if el.nil?
      begin
        other.any? {|other_el| el === other_el }
      rescue NoMethodError => e
        raise unless e.message =~ /any\?/
        el === other
      end
    }
  end
end

最近我尝试重新实现Enumerable中的所有方法,但是使用reduce而不是each作为基础。在这种情况下,我知道它实际上不能正常工作。

module Enumerable
  def all?
    return reduce(true) {|res, el| break false unless res; res && el } unless block_given?
    reduce(true) {|res, el| break false unless res; res && yield(el) }
  end

  def any?
    return reduce(false) {|res, el| break true if res || el } unless block_given?
    reduce(false) {|res, el| break true if res || yield(el) }
  end

  def collect
    reduce([]) {|res, el| res << yield(el) }
  end
  alias_method :map, :collect

  def count(item=undefined = Object.new)
    return reduce(0) {|res, el| res + 1 if el == item } unless undefined.equal?(item)
    unless block_given?
      return size if respond_to? :size
      return reduce(0) {|res, el| res + 1 }
    end
    reduce(0) {|res, el| res + 1 if yield el }
  end

  def detect(ifnone=nil)
    reduce(ifnone) {|res, el| if yield el then el end unless res }
  end
  alias_method :find, :detect

  def drop(n=1)
    reduce([]) {|res, el| res.tap { res << el unless n -= 1 >= 0 }}
  end

  def drop_while
    reduce([]) {|res, el| res.tap { res << el unless yield el }}
  end

  def each
    tap { reduce(nil) {|_, el| yield el }}
  end

  def each_with_index
    tap { reduce(-1) {|i, el| (i+1).tap {|i| yield el, i }}}
  end

  def find_all
    reduce([]) {|res, el| res.tap {|res| res << el if yield el }}
  end
  alias_method :select, :find_all

  def find_index(item=undefined = Object.new)
    return reduce(-1) {|res, el| break res + 1 if el == item } unless undefined.equals?(item)
    reduce(-1) {|res, el| break res + 1 if yield el }
  end

  def grep(pattern)
    return reduce([]) {|res, el| res.tap {|res| res << el if pattern === el }} unless block_given?
    reduce([]) {|res, el| res.tap {|res| res << yield(el) if pattern === el }}
  end

  def group_by
    reduce(Hash.new {|hsh, key| hsh[key] = [] }) {|res, el| res.tap { res[yield el] = el }}
  end

  def include?(obj)
    reduce(false) {|res, el| break true if res || el == obj }
  end

  def reject
    reduce([]) {|res, el| res.tap {|res| res << el unless yield el }}
  end
end

1
Jörg,你的斐波那契数列例子有问题: fibs.take(7) => [[0, 0], [1, 1]] - Alexey
@Alexey:你说得对。这就是我把一个酷炫的两行代码变成一个酷炫的一行代码后没有测试它的结果;-) 回家后会进行调查。 - Jörg W Mittag

1

从数组中初始化多个值:

a = [1,2,3]
b, *c = a

assert_equal [b, c], [1, [2,3]]

d, = a
assert_equal d, a[0]

0

探索字符串的布尔成员方法:

"".methods.sort.grep(/\?/)

探索字符串特定的方法:

"".methods.sort - [].methods

0

我的方法如下:

使用相同的表达式初始化数组元素:

5.times.map { some_expression }

使用相同的值初始化数组:

[value]*5

计算数组元素之和:

[1,2,3].reduce(0, &:+)

找出所有符合条件的索引:

a.each_with_index.find_all { |e, i| some_predicate(e) }.map(&:last)

我猜reduce只是一个例子,因为你有[1,2,3].sum。最后一段代码可以写成"a.each_with_index.map { |e, i| i if some_predicate(e) }.compact",虽然它可能会生成一个更大的中间数组。 - tokland
糟糕,看起来Enumerable#sum实际上是一个扩展,而不是原生的Ruby。 - tokland
除非“value”是不可变的,否则“[value] * 5”可能会给你带来大量的麻烦。 - Andrew Grimm
@Andrew,在可变情况下使用5.times.map { some_expression },或者尽量减少代码中的变异。 - Alexey

0

虽然不是代码片段,但我喜欢这些通用的构造(我只展示如何使用它们,实现很容易在网上找到)。

数组转哈希表(to_hashmash,思路相同,请参考Facets的实现):

>> [1, 2, 3].mash { |k| [k, 2*k] } 
=> {1=>2, 2=>4, 3=>6}

Map + select/detect: 您想要进行映射并仅获取第一个结果(因此 map { ... }.first 将效率低下):

>> [1, 2, 3].map_select { |k| 2*k if k > 1 } 
=> [4, 6]

>> [1, 2, 3].map_detect { |k| 2*k if k > 1 } 
=> 4

惰性迭代(lazy_map,lazy_select,...)。例如:

>> 1.upto(1e100).lazy_map { |x| 2 *x }.first(5)
=> [2, 4, 6, 8, 10]

Facets、Hashery 和 Tom 的其他库是一个宝藏,包含各种伟大的 Ruby 代码,不仅与集合和迭代器有关。 - Jörg W Mittag
def mash &body; Hash[map(&body)] end - Alexey
顺便说一下:最后那个真的很让我烦恼。例如,在Scala中,map等操作保证返回与调用它们时相同的集合类型。在Ruby中,它们总是返回一个具体、严格、完全实现的Array。对于HashTree来说,这只是令人恼火,但对于像Enumerator这样潜在的无限数据结构来说,这就非常致命了。如果map只是返回调用它的内容,你就不需要使用lazy_map,它会正常工作。 - Jörg W Mittag
@Jörg,我完全同意Ruby普遍使用数组不是一个明智的决定。Python通过惰性生成器解决了这个问题,但我仍然不知道Ruby将如何以一种好的方式解决它。 - tokland
在大多数实际情况下,我更喜欢Ruby的方法,而不是Scala的方法。在Scala中,我经常不得不将结果序列转换为另一种类型的序列,因为几乎总是只需要数组 :) - Alexey

0

计算满足任一条件或另一条件的项目数量:

items.count do |item|
  next true unless first_test?(item)
  next true unless second_test?(item)
  false
end

count 表示你不需要执行 i = 0i += 1

next 表示你可以完成该块的迭代,并仍然提供答案,而不必等到结束。

(如果你愿意,你可以用单行代码 ! second_test?(item) 替换该块的最后两行,但那会让它看起来更凌乱)


为什么不这样做呢?items.count { |i| first_test?(i) or second_test?(i) } - Alexey
@Alexey:除非真正的代码非常冗长,无法轻松地放在一行上,否则你可以。 - Andrew Grimm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接