List comprehension in Ruby

后端 未结 17 2140
广开言路
广开言路 2020-12-02 07:01

To do the equivalent of Python list comprehensions, I\'m doing the following:

some_array.select{|x| x % 2 == 0 }.collect{|x| x * 3}

Is ther

相关标签:
17条回答
  • 2020-12-02 07:34

    I discussed this topic with Rein Henrichs, who tells me that the best performing solution is

    map { ... }.compact
    

    This makes good sense because it avoids building intermediate Arrays as with the immutable usage of Enumerable#inject, and it avoids growing the Array, which causes allocation. It's as general as any of the others unless your collection can contain nil elements.

    I haven't compared this with

    select {...}.map{...}
    

    It's possible that Ruby's C implementation of Enumerable#select is also very good.

    0 讨论(0)
  • 2020-12-02 07:37

    Something like this:

    def lazy(collection, &blk)
       collection.map{|x| blk.call(x)}.compact
    end
    

    Call it:

    lazy (1..6){|x| x * 3 if x.even?}
    

    Which returns:

    => [6, 12, 18]
    
    0 讨论(0)
  • 2020-12-02 07:40

    There seems to be some confusion amongst Ruby programmers in this thread concerning what list comprehension is. Every single response assumes some preexisting array to transform. But list comprehension's power lies in an array created on the fly with the following syntax:

    squares = [x**2 for x in range(10)]
    

    The following would be an analog in Ruby (the only adequate answer in this thread, AFAIC):

    a = Array.new(4).map{rand(2**49..2**50)} 
    

    In the above case, I'm creating an array of random integers, but the block could contain anything. But this would be a Ruby list comprehension.

    0 讨论(0)
  • 2020-12-02 07:41

    I made a quick benchmark comparing the three alternatives and map-compact really seems to be the best option.

    Performance test (Rails)

    require 'test_helper'
    require 'performance_test_help'
    
    class ListComprehensionTest < ActionController::PerformanceTest
    
      TEST_ARRAY = (1..100).to_a
    
      def test_map_compact
        1000.times do
          TEST_ARRAY.map{|x| x % 2 == 0 ? x * 3 : nil}.compact
        end
      end
    
      def test_select_map
        1000.times do
          TEST_ARRAY.select{|x| x % 2 == 0 }.map{|x| x * 3}
        end
      end
    
      def test_inject
        1000.times do
          TEST_ARRAY.inject([]) {|all, x| all << x*3 if x % 2 == 0; all }
        end
      end
    
    end
    

    Results

    /usr/bin/ruby1.8 -I"lib:test" "/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/performance/list_comprehension_test.rb" -- --benchmark
    Loaded suite /usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
    Started
    ListComprehensionTest#test_inject (1230 ms warmup)
               wall_time: 1221 ms
                  memory: 0.00 KB
                 objects: 0
                 gc_runs: 0
                 gc_time: 0 ms
    .ListComprehensionTest#test_map_compact (860 ms warmup)
               wall_time: 855 ms
                  memory: 0.00 KB
                 objects: 0
                 gc_runs: 0
                 gc_time: 0 ms
    .ListComprehensionTest#test_select_map (961 ms warmup)
               wall_time: 955 ms
                  memory: 0.00 KB
                 objects: 0
                 gc_runs: 0
                 gc_time: 0 ms
    .
    Finished in 66.683039 seconds.
    
    15 tests, 0 assertions, 0 failures, 0 errors
    
    0 讨论(0)
  • 2020-12-02 07:42

    This is one way to approach this:

    c = -> x do $*.clear             
      if x['if'] && x[0] != 'f' .  
        y = x[0...x.index('for')]    
        x = x[x.index('for')..-1]
        (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
        x.insert(x.length, "end; $*")
        eval(x)
        $*)
      elsif x['if'] && x[0] == 'f'
        (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << x")
        x.insert(x.length, "end; $*")
        eval(x)
        $*)
      elsif !x['if'] && x[0] != 'f'
        y = x[0...x.index('for')]
        x = x[x.index('for')..-1]
        (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
        x.insert(x.length, "end; $*")
        eval(x)
        $*)
      else
        eval(x.split[3]).to_a
      end
    end 
    

    so basically we are converting a string to proper ruby syntax for loop then we can use python syntax in a string to do:

    c['for x in 1..10']
    c['for x in 1..10 if x.even?']
    c['x**2 for x in 1..10 if x.even?']
    c['x**2 for x in 1..10']
    
    # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    # [2, 4, 6, 8, 10]
    # [4, 16, 36, 64, 100]
    # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
    

    or if you don't like the way the string looks or having to use a lambda we could forego the attempt to mirror python syntax and do something like this:

    S = [for x in 0...9 do $* << x*2 if x.even? end, $*][1]
    # [0, 4, 8, 12, 16]
    
    0 讨论(0)
  • 2020-12-02 07:42

    Ruby 2.7 introduced filter_map which pretty much achieves what you want (map + compact):

    some_array.filter_map { |x| x * 3 if x % 2 == 0 }
    

    You can read more about it here.

    0 讨论(0)
提交回复
热议问题