How to map and remove nil values in Ruby

前端 未结 8 1579
野性不改
野性不改 2020-12-07 07:51

I have a map which either changes a value or sets it to nil. I then want to remove the nil entries from the list. The list doesn\'t need to be kept.

Thi

相关标签:
8条回答
  • 2020-12-07 08:14

    Ruby 2.7+

    There is now!

    Ruby 2.7 is introducing filter_map for this exact purpose. It's idiomatic and performant, and I'd expect it to become the norm very soon.

    For example:

    numbers = [1, 2, 5, 8, 10, 13]
    enum.filter_map { |i| i * 2 if i.even? }
    # => [4, 16, 20]
    

    In your case, as the block evaluates to falsey, simply:

    items.filter_map { |x| process_x url }
    

    "Ruby 2.7 adds Enumerable#filter_map" is a good read on the subject, with some performance benchmarks against some of the earlier approaches to this problem:

    N = 100_000
    enum = 1.upto(1_000)
    Benchmark.bmbm do |x|
      x.report("select + map")  { N.times { enum.select { |i| i.even? }.map{ |i| i + 1 } } }
      x.report("map + compact") { N.times { enum.map { |i| i + 1 if i.even? }.compact } }
      x.report("filter_map")    { N.times { enum.filter_map { |i| i + 1 if i.even? } } }
    end
    
    # Rehearsal -------------------------------------------------
    # select + map    8.569651   0.051319   8.620970 (  8.632449)
    # map + compact   7.392666   0.133964   7.526630 (  7.538013)
    # filter_map      6.923772   0.022314   6.946086 (  6.956135)
    # --------------------------------------- total: 23.093686sec
    # 
    #                     user     system      total        real
    # select + map    8.550637   0.033190   8.583827 (  8.597627)
    # map + compact   7.263667   0.131180   7.394847 (  7.405570)
    # filter_map      6.761388   0.018223   6.779611 (  6.790559)
    
    0 讨论(0)
  • 2020-12-07 08:17

    Try using reduce or inject.

    [1, 2, 3].reduce([]) { |memo, i|
      if i % 2 == 0
        memo << i
      end
    
      memo
    }
    

    I agree with the accepted answer that we shouldn't map and compact, but not for the same reasons.

    I feel deep inside that map then compact is equivalent to select then map. Consider: map is a one-to-one function. If you are mapping from some set of values, and you map, then you want one value in the output set for each value in the input set. If you are having to select before-hand, then you probably don't want a map on the set. If you are having to select afterwards (or compact) then you probably don't want a map on the set. In either case you are iterating twice over the entire set, when a reduce only needs to go once.

    Also, in English, you are trying to "reduce a set of integers into a set of even integers".

    0 讨论(0)
  • 2020-12-07 08:20

    One more way to accomplish it will be as shown below. Here, we use Enumerable#each_with_object to collect values, and make use of Object#tap to get rid of temporary variable that is otherwise needed for nil check on result of process_x method.

    items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}
    

    Complete example for illustration:

    items = [1,2,3,4,5]
    def process x
        rand(10) > 5 ? nil : x
    end
    
    items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}
    

    Alternate approach:

    By looking at the method you are calling process_x url, it is not clear what is the purpose of input x in that method. If I assume that you are going to process the value of x by passing it some url and determine which of the xs really get processed into valid non-nil results - then, may be Enumerabble.group_by is a better option than Enumerable#map.

    h = items.group_by {|x| (process x).nil? ? "Bad" : "Good"}
    #=> {"Bad"=>[1, 2], "Good"=>[3, 4, 5]}
    
    h["Good"]
    #=> [3,4,5]
    
    0 讨论(0)
  • 2020-12-07 08:28

    Definitely compact is the best approach for solving this task. However, we can achieve the same result just with a simple subtraction:

    [1, nil, 3, nil, nil] - [nil]
     => [1, 3]
    
    0 讨论(0)
  • 2020-12-07 08:29

    You could use compact:

    [1, nil, 3, nil, nil].compact
    => [1, 3] 
    

    I'd like to remind people that if you're getting an array containing nils as the output of a map block, and that block tries to conditionally return values, then you've got code smell and need to rethink your logic.

    For instance, if you're doing something that does this:

    [1,2,3].map{ |i|
      if i % 2 == 0
        i
      end
    }
    # => [nil, 2, nil]
    

    Then don't. Instead, prior to the map, reject the stuff you don't want or select what you do want:

    [1,2,3].select{ |i| i % 2 == 0 }.map{ |i|
      i
    }
    # => [2]
    

    I consider using compact to clean up a mess as a last-ditch effort to get rid of things we didn't handle correctly, usually because we didn't know what was coming at us. We should always know what sort of data is being thrown around in our program; Unexpected/unknown data is bad. Anytime I see nils in an array I'm working on, I dig into why they exist, and see if I can improve the code generating the array, rather than allow Ruby to waste time and memory generating nils then sifting through the array to remove them later.

    'Just my $%0.2f.' % [2.to_f/100]
    
    0 讨论(0)
  • 2020-12-07 08:34

    In your example:

    items.map! { |x| process_x url } # [1, 2, 3, 4, 5] => [1, nil, 3, nil, nil]
    

    it does not look like the values have changed other than being replaced with nil. If that is the case, then:

    items.select{|x| process_x url}
    

    will suffice.

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