【问题标题】:What is a more "ruby way" to write this code?编写此代码的更“红宝石方式”是什么?
【发布时间】:2010-06-15 06:37:16
【问题描述】:

这是我的学生(我是助教)在 c 语言中的家庭作业,我正在尝试学习 Ruby,所以我想我会编写它。目标是从重定向文件中读取整数并打印一些简单信息。文件的第一行是元素的个数,然后每个整数占一行。

这段代码有效(尽管可能效率低下),但我怎样才能使代码更像 Ruby?

#!/usr/bin/ruby -w

# first line is number of inputs (Don't need it)
num_inputs = STDIN.gets.to_i

# read inputs as ints
h = Hash.new
STDIN.each do |n|
  n = n.to_i
  h[n] = 1 unless h[n] and h[n] += 1      
end

# find smallest mode
h.sort.each do |k,v|
  break puts "Mode is: #{k}", "\n" if v == h.values.max
end

# mode unique?
v = h.values.sort
print "Mode is unique: "
puts v.pop == v.pop, "\n"

# print number of singleton odds, 
#       odd elems repeated odd number times in desc order
#       even singletons in desc order
odd_once = 0
odd = Array.new
even = Array.new
h.each_pair do |k, v|
  odd_once += 1 if v == 1 and k.odd?
  odd << k if v.odd?
  even << k if v == 1 and k.even?
end
puts "Number of elements with an odd value that appear only once: #{odd_once}", "\n"
puts "Elements repeated an odd number of times:"
puts odd.sort.reverse, "\n"
puts "Elements with an even value that appear exactly once:"
puts even.sort.reverse, "\n"

# print fib numbers in the hash
class Fixnum
  def is_fib?
    l, h = 0, 1
    while h <= self
      return true if h == self
      l, h = h, l+h
    end
  end
end
puts "Fibonacci numbers:"
h.keys.sort.each do |n|
  puts n if n.is_fib?
end

【问题讨论】:

    标签: ruby coding-style


    【解决方案1】:

    我不知道这是否是“更多的 Ruby 方式”。至少是一种更“高阶”的方式,FWIW。

    # first line is number of inputs (Don't need it), thus drop the first line
    # read inputs as ints
    h = ARGF.drop(1).reduce(Hash.new(0)) {|h, n| h.tap {|h| h[n.to_i] += 1 }}
    

    这里不多说。我们不是简单地循环 ARGF 并设置哈希键,而是使用 reduce 让它为我们完成工作。我们使用默认值为0 的哈希,而不是手动检查键是否存在。

    我们使用Enumerable#drop 来简单地删除第一行。

    ARGF 是从 Perl 中窃取的一个非常酷的功能(就像大多数脚本功能一样):如果您只是将脚本称为 script.rb 而不带参数,那么 ARGF 是标准输入。但是,如果您将脚本称为script.rb a.txt b.txt,那么Ruby 会将所有参数解释为文件名,打开所有文件进行读取,ARGF 将是它们内容的串联。这使您可以非常快速地编写可以通过标准输入或文件获取其输入的脚本。

    # find smallest mode
    modes = h.group_by(&:last).sort.last.last.map(&:first).sort
    puts "Mode is: #{modes.first}"
    

    Ruby 没有明确的键值对类型,相反,大多数对哈希的循环操作都使用二元素数组。这允许我们使用Array#firstArray#last 来引用键和值。

    在这种特殊情况下,我们使用Enumerable#group_by 将哈希分组到不同的桶中,我们使用的分组标准是last 方法,即我们哈希中的值是频率。换句话说,我们按频率分组。

    如果我们现在对结果哈希进行排序,最后一个元素是频率最高的元素(即模式)。我们取它的最后一个元素(键值对的值),然后是它的最后一个元素,这是一个键值对数组(number =&gt; frequency),我们从中提取键(数字)并对其进行排序。

    [注意:只需将每个中间阶段的结果打印出来,就更容易理解了。只需将上面的 modes = ... 行替换为以下内容:

    p modes = h.tap(&method(:p)).
      group_by(&:last).tap(&method(:p)).
      sort.tap(&method(:p)).
      last.tap(&method(:p)).
      last.tap(&method(:p)).
      map(&:first).tap(&method(:p)).
      sort
    

    ]

    modes 现在是一个排序数组,其中包含具有该特定频率的所有数字。如果我们取第一个元素,我们有最小的模式。

    # mode unique?
    puts "Mode is #{unless modes.size == 1 then '*not* ' end}unique."
    

    如果数组的大小不是1,那么模式不是唯一的。

    # print number of singleton odds, 
    #       odd elems repeated odd number times in desc order
    #       even singletons in desc order
    odds, evens = h.select {|_,f|f==1}.map(&:first).sort.reverse.partition(&:odd?)
    

    看起来这里发生了很多事情,但实际上很简单。您从等号后开始阅读,然后从左到右阅读。

    1. 我们选择哈希值(即频率)为1的所有项目。 IOW,我们选择所有的单例。
    2. 我们将所有生成的键值对映射到它们的第一个元素,即数字 - 我们丢弃频率的 IOW。
    3. 我们对列表进行排序
    4. 然后反转它(对于较大的列表,我们应该从反转开始,因为这非常浪费 CPU 周期和内存)
    5. 最后,我们将数组分成两个数组,一个包含所有奇数,另一个包含所有偶数
    6. 现在我们终于看到等号的左侧:Enumerable#partition 返回一个二元素数组,其中包含两个具有分区元素的数组,我们使用 Ruby 的解构赋值将这两个数组分配给两个变量

      puts "奇数值只出现一次的元素个数:#{odds.size}"

    现在我们有了一个奇数单例列表,它们的数量就是列表的大小。

    puts "Elements repeated an odd number of times: #{
      h.select {|_, f| f.odd?}.map(&:first).sort.reverse.join(', ')
    }"
    

    这与上面的非常相似:选择所有奇数频率的数字,映射出键(即数字),排序,反转,然后通过将它们连接在一起并用逗号和空格将它们转换为字符串。

    puts "Elements with an even value that appear exactly once: #{evens.join(', ')}"
    

    同样,既然我们有一个偶数单例列表,打印它们只需用逗号连接列表元素即可。

    # print fib numbers in the hash
    

    我不想重构这个算法以提高效率并专门用于记忆。我只是做了一些小的调整。

    class Integer
    

    算法中没有任何东西依赖于特定大小的数字,所以我将方法拉到Integer 类中。

      def fib?
    

    我删除了is_ 前缀。这是一个布尔方法这一事实已经在问号中明确了。

        l, h = 0, 1
        while h <= self
          return true if h == self
          l, h = h, l+h
        end
      end
    end
    puts "Fibonacci numbers: #{h.keys.sort.select(&:fib?).join(', ')}"
    

    这可能不需要太多解释:取出键,对它们进行排序,选择所有斐波那契数并用逗号将它们连接在一起。

    这里有一个关于如何重构这个算法的想法。 a very interesting implementation of Fibonacci 使用带有默认值的 Hash 用于记忆:

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

    看起来有点像这样:

    class Integer
      @@fibs = {0 => 0, 1 => 1}.tap do |fibs|
        fibs.default_proc = ->(fibs, n) { fibs[n] = fibs[n-1] + fibs[n-2] }
      end
    
      def fib?
        i = 0
        until @@fibs[i += 1] > self
          break true if @@fibs[i] == self
        end
      end
    end
    puts "Fibonacci numbers: #{h.keys.sort.select(&:fib?).join(', ')}"
    

    如果有人能想出一种优雅的方式来摆脱i = 0i += 1 和整个until 循环,我将不胜感激。

    【讨论】:

    • (1..self).any? { |i| return false if @@fibs[i] &gt; self; @@fibs[i] == self } 怎么样?当然是do end,而不是{};那样输入是因为我不知道如何在评论框中换行。
    • 感谢您周到的回答。我假设您正在尝试使每个操作尽可能实用(“高阶”?)我需要一些时间来消化所有这些新信息。
    【解决方案2】:

    这是上半年的一些想法......

    STDIN.gets # just need to get past the first value - not needed, as already noticed
    
    h = Hash.new(0)                    # use the default value argument to Hash#new, 
    STDIN.each { |n| h[n.to_i] += 1 }  # so don't have to test for existence
    
    # code seems to be seeking the largest, comment says "smallest"
    # Hash#invert switches keys & values, works here if there's only one mode,
    # otherwise presents one random key for the modal value
    modal_value = h.values.max
    mode = h.invert[modal_value]
    puts "Mode is: #{mode}"
    
    # OK, so mode may not be unique?
    uniqueness = h.select { |k, v| v == modal_value}.size == 1 ? '' : 'NOT'
    puts "Mode is #{uniqueness} unique"
    
    #singleton odds ...
    singleton_odd_count = h.select { |k,v| v == 1 && k % 2 == 1 }.map { |k, v| k }.size
    # ...and evens
    singleton_evens = h.select { |k,v| v == 1 && k % 2 == 0 }.map { |k, v| k }
    # odd odd counts
    odd_odd_count = h.select { |k,v| v % 2 == 1 && k % 2 == 1 }.map { |k, v| k }
    

    【讨论】:

    • 嘿,这里是另一个 Ruby 新手——我看到一个分配给 modal_value,但随后引用了 h_modal_value。这只是一个错字,还是 Ruby 魔法?如果是后者,是什么魔法?
    • @Ben - 没有“魔法”,只是尝试将转录中的变量从 irb 重命名为答案失败。非常平凡,真的:-)
    【解决方案3】:

    我认为您的代码大部分都很好。我一眼就能看出效率低下的唯一一件事是你的斐波那契检查——你每次都在重新计算斐波那契数列,重复了很多工作。计算一次斐波那契数列会更有意义,直到输入集中的最大值,然后只需检查每个值以查看它是否在该数组中。

    此外,您可以通过将值传递给Hash.new 来简化顶部的哈希初始化——这将成为它尚未看到的任何键的默认值。所以你可以简单地这样做:

    h = Hash.new(0)
    STDIN.each {|n| h[n.to_i] += 1}
    

    否则,单个计算没有问题。让我印象深刻的是“不像 Ruby”,只是有一个意大利面条脚本,它计算了六种不同的东西并将它们吐出来。将这些不同的数学函数中的每一个包装到一个方法中,并将这些方法放入一个模块中会更优雅。然后你的脚本可以只包含模块,用你的哈希调用方法,并输出它需要的任何东西。

    【讨论】:

    • 谢谢,错过了。是的,这项任务很复杂,但并不多余或重要到足以将所有内容都放入方法中。 :)
    • 我以为你是在 Ruby 中进行练习的。您询问了执行此操作的“Ruby 方式”,我回答了。 Ruby 方式(实际上是敏捷方式)是将问题分解成更小的块,这些块可以单独测试,并且可以适应变化,而不会让整叠 Jenga 积木翻倒。在课堂练习之外,永远不要认为一个问题对于良好的结构来说不是“多余的或足够重要的”。商业世界充满了蹩脚的一次性代码,它们永远不会完全消亡或被丢弃。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-17
    • 2019-06-22
    • 2013-05-22
    相关资源
    最近更新 更多